import copy
import numpy
import re
import netCDF4
import textwrap
import collections
from .utils import equals, _set_tolerances, Comparison

def _convert_data_to_numpy(v, squeeze=False):
    '''

    Private function used by Variable and its subclasses in their
    __getitem__ methods.

    If the data of `v`, a variable, is a file pointer then convert it
    in place to a numpy array. If `squeeze` is True then remove size 1
    dimensions before returning.

    If the data of `v` is already a numpy array then do nothing, even
    if `squeeze` is True.
    
    '''
    try:
        v._data
    except AttributeError:
        raise AttributeError("'"+v.__class__.__name__+"' object has no data")

    if not isinstance(v._data, numpy.ndarray):
        # Overwrite v._data with a numpy array 
        v._data = v._data[...]
        if squeeze:
            # Remove size 1 dimensions
            v._data = numpy.squeeze(v._data)
        # Turn a scalar into a one dimensional, size 1 array
        if v._data.shape is ():
            v._data = numpy.expand_dims(v._data, axis=0)

    return v
#--- End: def _convert_data_to_numpy

def _parse_index(v, index):
    '''

    Private function used by Variable and its subclasses in their
    __getitem__ methods.

    Expand the index input to __getitem__ so that it has one slice per
    dimension of the data of `s`, a variable. Might return Ellipsis if
    the converted index is equivalent to it.
    
    '''
    # Fix index, handling ellipsis and incomplete slices (copied
    # and modified from numpy)
    if not isinstance(index, tuple): index = (index,)
    fixed = []
    length, dims = len(index), len(v.shape)
    for slice_ in index:
        if slice_ is Ellipsis:
            fixed.extend([slice(None)] * (dims-length+1))
            length = len(fixed)
        elif isinstance(slice_, (int, long)):
            if slice_ == -1:                    
                fixed.append(slice(slice_, None, 1))
            else:
                fixed.append(slice(slice_, slice_+1, 1))
        else:
            if isinstance(slice_, tuple):
                slice_ = list(slice_)
            fixed.append(slice_)

    index = tuple(fixed)
    if len(index) < dims:
        index += (slice(None),) * (dims-len(index))
            
    return index
#--- End: def _parse_index

def _slice_data(v, index):
    '''

    Private function used by Variable and its subclasses in their
    __getitem__ methods.

    Use index to slice the data array of `v`, a variable,
    in-place. The slice is done one dimension at a time. The `index`
    argument is expected to contain a slice for each dimension of
    v._data. The variable retains its size one dimensions on output.
    
    '''
    full_slice = [slice(None)] * v.ndim

    # Return now if the index is ellipsis
    try:
        if index == tuple(full_slice):
            return v
    except ValueError: 
        pass

    # Still here? Then slice the data one dimension at a time. Doing
    # it this way allows for more than one dimension to be an array of
    # booleans.
    for pos in xrange(len(index)):
        i      = full_slice[:]
        i[pos] = index[pos]
        v._data = v._data[tuple(i)]
        # Reinstate any size 1 dimensions which have been dropped
        if v._data.ndim < len(index):
            v._data = numpy.expand_dims(v._data, axis=pos)
        if v._data.shape[pos] == 0:
            raise IndexError('index for dimension '+str(pos)+' out of range')

    return v
#--- End: def _slice_data

def _find_properties(v, nc=False):
    '''

    Return a set of writable, public attribute names, excluding
    methods.

    An attribute created by assigning directly to the `__dict__`
    dictionary (as opposed to using setattr) will not appear in this
    set.

    Parameters
    ----------
    nc : bool
        If False then exclude attributes whose names begin 'nc'.

    Returns
    -------
        A set of attribute names.

    '''
    # See method __setattr__ for details on how the _properties set is
    # created
    
    try:
        properties = v._properties
    except AttributeError:
        return set(())

    if not nc:
        properties = set([x for x in properties if not x.startswith('nc')])
            
    return properties
#--- End: def _find_properties

class Variable(object):
    '''
    
    Variable(**kwargs)

    Base class for a space and its components.

    The following objects are all subclasses of Variable: a space, a
    coordinate, coordinate bounds and cell measures.

    Refer to the `cf` module for more details.

    Parameters
    ----------
    **kwargs :
        The new variable is given attributes named by the keywords,
        whose values are deep copies the keyword values.

    Overloaded operators
    --------------------
    Refer to `cf`.

    Attributes
    ----------   
    array : numpy array
        A numpy array deep copy of the data.
    dtype : numpy.dtype
        Describes the format of the elements in the data array (refer
        to `numpy.ndarray.dtype`).
    ncvar : string, optional 
        Contains the name of the variable in the original netCDF
        file. If present when the space is written to a netCDF file,
        then used for the output netCDF variable name.
    ndim : int
        The data array's number of dimensions (refer to
        `numpy.ndarray.ndim`).
    shape : tuple of ints
        The shape of the data array (refer to `numpy.ndarray.shape`).
    size : int
        The number of elements in the data array (refer to
        `numpy.ndarray.size`).
    type : type
        The type of the data object, which is either a numpy array or
        a file pointer.
    varray : numpy array
        A numpy view of the data.
    _atol : None or float, optional
        Absolute tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.
    _rtol : None or float, optional
        Relative tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.

    Methods
    -------
    copy        : Create a copy.
    dump        : Return a string containing a full description of the
                  variable.
    equals      : Determine whether two variables are congruent with
                  each other.
    extract     : Determine whether a variable matches phenomena
                  criteria.
    first_datum : Return the first element of the data without
                  replacing a file pointer with a numpy array.
    last_datum  : Return the last element of the data without
                  replacing a file pointer with a numpy array.
    name        : Return the name (standard_name, long_name or ncvar).
    ncatts      : Return the set of attributes which are netCDF
                  attributes.
    properties  : Return a set of writable, public attributes.
    repoint     : Revert the data to a file pointer.
 
    '''
    def __init__(self, **kwargs):

        for attr in kwargs:
            value = kwargs[attr] 
            if isinstance(value, netCDF4.Variable):
                setattr(self, attr, value)
            else:
                try:
                    setattr(self, attr, copy.deepcopy(value))
                except RuntimeError:
                    try:
                        setattr(self, attr, copy.copy(value))
                    except RuntimeError:
                        setattr(self, attr, value)
    #--- End: def Variable.__init__

    def __deepcopy__(self, memo):
        '''

    Used if copy.deepcopy is called on the variable.
    
    If a deep copy of a variable's attribute raises an exception then
    a shallow copy is attempted. If the shallow copy also raises an
    exception then the variable's attribute is assigned to the
    returned copy.

    In particular, netCDF4.Variable instances are copied by
    assignment.

    Returns
    -------
        A deep copy of the variable, with the possibility of
        exceptions described above.
        
    See also
    --------
    copy
    ''' 
        return self.copy(shallow=(), omit=())
    #--- End: def Variable.__deepcopy__

    def __call__(self):
        '''

    Return a deep copy of the variable, identical to calling
    copy.deepcopy on the variable.

    Returns
    -------
        A deep copy of the variable.
        
    See also
    --------
    copy
    ''' 
        return self.copy(shallow=(), omit=())
    #--- End Variable.__call__


    def __add__(self, other):
        '''
        
     Implement the + binary operation to modify the variable's data.
     
     The right hand side of the operation may be another variable (in
     which case its data are used) or any object allowed by the
     equivalent numpy operation.

     Returns
     -------
         A variable.
     '''
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))
        new._data = self.varray.__add__(y)
        return new
    #--- End: def Variable._add___

    def __sub__(self, other):
        '''
        
    Implement the - binary operation to modify the variable's data.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))
        new._data = self.varray.__sub__(y)
        return new
    #--- End: def Variable.__sub__

    def __mul__(self, other):
        '''

    Implement the * binary operation to modify the variable's data.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))
        new._data = self.varray.__mul__(y)
        return new
    #--- End: def Variable.__mul__

    def __div__(self, other):
        '''

    Implement the / binary operation to modify the variable's data.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))
        new._data = self.varray.__div__(y)
        return new
    #--- End: def Variable.__div__

    def __truediv__(self, other):
        '''
        
    Implement the / binary operation to modify the variable's data
    when __future__.division is in effect.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.

    '''
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))
        new._data = self.varray.__truediv__(y)
        return new
    #--- End: def Variable.__truediv__

    def __floordiv__(self, other):
        '''
        
    Implement the // binary operation to modify the variable's data.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.

    '''
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))
        new._data = self.varray.__floordiv__(y)
        return new
    #--- End: def Variable.__floordiv__
        
    def __mod__(self, other):
        '''
        
    Implement the % binary operation to modify the variable's data.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.

    '''
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))
        new._data = self.varray.__mod__(y)
        return new
    #--- End: def Variable.__mod__

    def __pow__(self, other, modulo=None):
        '''
        
    Implement the pow() and ** binary operations to modify the
    variable's data.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.

    '''
        if modulo is not None:
            raise RuntimeError('3-argument power not supported')
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))         
        new._data = self.varray.__pow__(y, modulo)
        return new
    #--- End: def Variable.__pow__
    
    def __and__(self, other):
        '''
        
    Implement the & binary operation to modify the variable's data.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.

    '''
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))
        new._data = self.varray.__add__(y)
        return new
    #--- End: def Variable.__xor__
    
    def __xor__(self, other):
        '''
        
    Implement the ^ binary operation to modify the variable's data.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.

    '''
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))
        new._data = self.varray.__xor__(y)
        return new
    #--- End: def Variable.__xor__
    
    def __or__(self, other):
        '''
        
    Implement the | binary operation to modify the variable's data.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation

    Returns
    -------
        A variable.
    '''
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))
        new._data = self.varray.__or__(y)
        return new
    #--- End: def Variable.__or__

    def __iadd__(self, other):
        '''
        
     Implement the += augmented arithmetic assignments to modify the
     variable's data in place
     
     The right hand side of the operation may be another variable (in
     which case its data are used) or any object allowed by the
     equivalent numpy operation.

     Returns
     -------
         A variable.
     '''
        # This potentially faster than falling back to the normal
        # method, since any components which are themselves Variable
        # instances do not get copied.
        x = self.varray
        try:
            y = other.varray
        except AttributeError:
            y = other
        x.__iadd__(y)
        return self
    #--- End: def Variable._iadd___

    def __isub__(self, other):
        '''
        
    Implement the -= augmented arithmetic assignments to modify the
    variable's data in place

    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        # This is potentially faster than falling back to the normal
        # method, since any components which are themselves Variable
        # instances do not get copied.
        x = self.varray
        try:
            y = other.varray
        except AttributeError:
            y = other
        x.__isub__(y)
        return self
    #--- End: def Variable.__isub__

    def __imul__(self, other):
        '''

    Implement the *= augmented arithmetic assignments to modify the
    variable's data in place

    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        # This potentially faster than falling back to the normal
        # method, since any components which are themselves Variable
        # instances do not get copied.
        x = self.varray
        try:
            y = other.varray
        except AttributeError:
            y = other
        x.__imul__(y)
        return self
    #--- End: def Variable.__imul__

    def __idiv__(self, other):
        '''

    Implement the /= augmented arithmetic assignments to modify the
    variable's data in place

    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        # This potentially faster than falling back to the normal
        # method, since any components which are themselves Variable
        # instances do not get copied.
        x = self.varray
        try:
            y = other.varray
        except AttributeError:
            y = other
        x.__idiv__(y)
        return self
    #--- End: def Variable.__idiv__

    def __itruediv__(self, other):
        '''
        
    Implement the /= augmented arithmetic assignments to modify the
    variable's data in place when __future__.division is in effect.

    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        # This potentially faster than falling back to the normal
        # method, since any components which are themselves Variable
        # instances do not get copied.
        x = self.varray
        try:
            y = other.varray
        except AttributeError:
            y = other
        x.__itruediv__(y)
        return self
     #--- End: def Variable.__itruediv__

    def __ifloordiv__(self, other):
        '''
        
    Implement the //= augmented arithmetic assignments to modify the
    variable's data in place.

    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        # This potentially faster than falling back to the normal
        # method, since any components which are themselves Variable
        # instances do not get copied.
        x = self.varray
        try:
            y = other.varray
        except AttributeError:
            y = other
        x.__ifloordiv__(y)
        return self
    #--- End: def Variable.__ifloordiv__
        
    def __imod__(self, other):
        '''
        
    Implement the %= augmented arithmetic assignments to modify the
    variable's data in place.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        # This potentially faster than falling back to the normal
        # method, since any components which are themselves Variable
        # instances do not get copied.
        x = self.varray
        try:
            y = other.varray
        except AttributeError:
            y = other
        x.__imod__(y)
        return self
    #--- End: def Variable.__imod__

    def __ipow__(self, other, modulo=None):
        '''

    Implement the *= augmented arithmetic assignments to modify the
    variable's data in place.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        # This potentially faster than falling back to the normal
        # method, since any components which are themselves Variable
        # instances do not get copied.
        x = self.varray
        try:
            y = other.varray
        except AttributeError:
            y = other
        x.__ipow__(y)
        return self
    #--- End: def Variable.__ipow__
    
    def __iand__(self, other):
        '''
        
    Implement the &= augmented arithmetic assignments to modify the
    variable's data in place.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        # This potentially faster than falling back to the normal
        # method, since any components which are themselves Variable
        # instances do not get copied.
        x = self.varray
        try:
            y = other.varray
        except AttributeError:
            y = other
        x.__iand__(y)
        return self        
    #--- End: def Variable.__iand__
    
    def __ixor__(self, other):
        '''
        
    Implement the ^= augmented arithmetic assignments to modify the
    variable's data in place.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        # This potentially faster than falling back to the normal
        # method, since any components which are themselves Variable
        # instances do not get copied.
        x = self.varray
        try:
            y = other.varray
        except AttributeError:
            y = other
        x.__ixor__(y)
        return self
    #--- End: def Variable.__ixor__
    
    def __ior__(self, other):
        '''
        
    Implement the |= augmented arithmetic assignments to modify the
    variable's data in place.
    
    The right hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation

    Returns
    -------
        A variable.
    '''
        # This potentially faster than falling back to the normal
        # method, since any components which are themselves Variable
        # instances do not get copied.
        x = self.varray
        try:
            y = other.varray
        except AttributeError:
            y = other
        x.__ior__(y)
        return self
    #--- End: def Variable.__ior__

    def __radd__(self, other):
        '''
        
     Implement the + binary operation to modify the variable's data.
     
     The left hand side of the operation may be another variable (in
     which case its data are used) or any object allowed by the
     equivalent numpy operation.

     Returns
     -------
         A variable.
     '''
        return self.__add__(other)
    #--- End: def Variable.__radd___

    def __rsub__(self, other):
        '''
        
    Implement the - binary operation to modify the variable's data.
    
    The left hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        return self.__sub__(other)
    #--- End: def Variable.__rsub__

    def __rmul__(self, other):
        '''

    Implement the * binary operation to modify the variable's data.
    
    The left hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.        
    '''
        return self.__mul__(other)
    #--- End: def Variable.__rmul__

    def __rdiv__(self, other):
        '''

    Implement the / binary operation to modify the variable's data.
    
    The left hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))
        new._data = self.varray.__rdiv__(y)
        return new
    #--- End: def Variable.__rdiv__

    def __rtruediv__(self, other):
        '''
        
    Implement the / binary operation to modify the variable's data
    when __future__.division is in effect.
    
    The left hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))
        new._data = self.varray.__rtruediv__(y)
        return new
    #--- End: def Variable.__rtruediv__

    def __rfloordiv__(self, other):
        '''
        
    Implement the // binary operation to modify the variable's data.
    
    The left hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''        
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))
        new._data = self.varray.__rfloordiv__(y)
        return new
    #--- End: def Variable.__rfloordiv__
        
    def __rmod__(self, other):
        '''
        
    Implement the % binary operation to modify the variable's data.
    
    The left hand side of the operation may be another variable (in
    which case its data are used) or any object allowed by the
    equivalent numpy operation.

    Returns
    -------
        A variable.
    '''
        try:
            y = other.varray
        except AttributeError:
            y = other
        new = self.copy(omit=('_data',))
        new._data = self.varray.__rmod__(y)
        return new
    #--- End: def Variable.__rmod__

    def __neg_(self, other):
        '''
        
    Implement the - unary operation to modify the variable's data.
     
    Returns
    -------
        A variable.
    '''
        return self.__mul__(-1)
    #--- End: def Variable.__neg__

    def __pos__(self):
        '''
        
    Implement the + unary operation to modify the variable's data.
     
    Returns
    -------
        A variable.
    '''
        return self.copy()
    #--- End: def Variable.__pos__

    def __abs__(self):
         '''
         
    Implement the abs() unary operation to modify the variable's
    data.
     
    Returns
    -------
        A variable.
        '''
         new = self.copy(omit=('_data',))
         new._data = numpy.absolute(self.varray)
         return new
    #--- End: def Variable.__abs__

    def __invert__(self):
         '''
         
    Implement the ~ unary operation to modify the variable's data.
    
    Returns
    -------
        A variable.
    '''
         new = self.copy(omit=('_data',))
         new._data = ~self.varray
         return new
    #--- End: def Variable.__invert__
    
    def __eq__(self, other):
        '''
         
    Implement the == comparison operator to apply to the variable's
    data.
        
    Equality of numbers is to within a tolerance. Refer to `cf` for
    details.

    Arguments
    ---------
    other : scalar, list or numpy array
        If the argument is a variable, then its data are used for the
        comparison, otherwise the argument may be any object which is
        valid in a comparison with a numpy array.

    Returns
    -------
        A boolean numpy array indicating which elements of the data
        satisfy the comparison.

    '''            
        return self.__data_equals__(other, rtol=None, atol=None, override=False)
    #--- End: def Variable.__eq__       

    def __data_equals__(self, other, rtol=None, atol=None, override=True):
        '''

    As `__eq__` but allowing the numeric equality tolerances to be set
    explicitly.

    '''
        # Set rtol and atol
        rtol, atol = _set_tolerances(self, other, rtol=rtol, atol=atol, 
                                     override=override)

        try:
            other._data

        except AttributeError:
            # -------------------------------
            # `other` is a scalar or an array
            # -------------------------------
            try:
                # Numeric values
                return numpy.less_equal(numpy.absolute(self.varray-other),
                                        atol + rtol*numpy.absolute(other))
            except TypeError:
                # String values
                return self.varray == other  
        
        else: 
            # ---------------------
            # `other` is a Variable
            # ---------------------
            # Check for file pointers being the same or for one array
            # being a view of the other.
            if self._data is other._data:
                a = numpy.empty(self.shape, dtype=bool)
                a.fill(True)
                return a

            try:
                self._data.base
                other._data.base
            except AttributeError:
                pass
            else:
                if ((self._data.base is not None and self._data.base is other._data) or
                    (other._data.base is not None and other._data.base is self._data)):
                    a = numpy.empty(self.shape, dtype=bool)
                    a.fill(True)
                    return a

            x = self.varray
            y = other.varray
            try:
                # Numeric values
                return numpy.less_equal(numpy.absolute(x - y),
                                        atol + rtol*numpy.absolute(y))
            except TypeError:
                # String values
                return x == y

    #--- End: def Variable.__data_equals__           

    def __ne__(self, other): 
        '''

    Implement the != comparison operator to apply to the variable's
    data.
  
    Equality of numbers is to within a tolerance. Refer to `cf` for
    details.

    Arguments
    ---------
    other : variable, scalar, list or numpy array
        If the argument is a variable, then its data are used for the
        comparison, otherwise the argument may be any object which is
        valid in a comparison with a numpy array.

    Returns
    -------
        A boolean numpy array indicating which elements of the data
        satisfy the comparison.

    '''
        return ~(self.__eq__(other))
    #--- End: def Variable.__ne__

    def __le__(self, other):
        '''

    Implement the <= comparison operator to apply to the variable's
    data.

    Arguments
    ---------
    other : variable, scalar, list or numpy array
        If the argument is a variable, then its data are used for the
        comparison, otherwise the argument may be any object which is
        valid in a comparison with a numpy array.

    Returns
    -------
        A boolean numpy array indicating which elements of the data
        satisfy the comparison.

    '''
        try:
            return self.varray <= other.varray
        except AttributeError:
            return self.varray <= other
    #--- End: def Variable.__le__

    def __lt__(self, other):
        '''

    Implement the < comparison operator to apply to the variable's
    data.

    Arguments
    ---------
    other : variable, scalar, list or numpy array
        If the argument is a variable, then its data are used for the
        comparison, otherwise the argument may be any object which is
        valid in a comparison with a numpy array.

    Returns
    -------
        A boolean numpy array indicating which elements of the data
        satisfy the comparison.

    '''
        try:
            return self.varray < other.varray
        except AttributeError:
            return self.varray < other
    #--- End: def Variable.__lt__        

    def __ge__(self, other):
        '''

    Implement the >= comparison operator to apply to the variable's
    data.

    Arguments
    ---------
    other : variable, scalar, list or numpy array
        If the argument is a variable, then its data are used for the
        comparison, otherwise the argument may be any object which is
        valid in a comparison with a numpy array.

    Returns
    -------
        A boolean numpy array indicating which elements of the data
        satisfy the comparison.

    '''
        try:
            return self.varray >= other.varray
        except AttributeError:
            return self.varray >= other
    #--- End: def Variable.__ge__

    def __gt__(self, other):
        '''

    Implement the > comparison operator to apply to the variable's
    data.

    Arguments
    ---------
    other : variable, scalar, list or numpy array
        If the argument is a variable, then its data are used for the
        comparison, otherwise the argument may be any object which is
        valid in a comparison with a numpy array.

    Returns
    -------
        A boolean numpy array indicating which elements of the data
        satisfy the comparison.

    '''
        try:
            return self.varray > other.varray
        except AttributeError:
            return self.varray > other
    #--- End: def Variable.__gt__

    def __getitem__(self, index):
        '''

    Implement indexing of the variable. Refer to `cf`.

    Returns
    -------
        A deep copy of the variable with the data and grid resized
        according to the given indices.

    '''     
        # Convert the data to a numpy array in place
        try:
            self = _convert_data_to_numpy(self, squeeze=False)
        except AttributeError:
            return new

        # Copy the variable
        new = self.copy()

        # Parse the index
        index = _parse_index(self, index)

        # Slice the data in-place, one dimension at a time
        new = _slice_data(new, index)

        return new
    #--- End: Variable.__getitem__

    def __setitem__(self, index, value):
        '''

    Implement assignment to indices of the variable

    Changes the variable's data in-place.

    '''

#        if len(self._data) == 1:            
#            if not isinstance(self._data[0], numpy.ndarray):
#                self[...]
#            self._data[0][i] = value
#
#        else:
#            pass # for now

        self[...]
        self._data[index] = value
    #--- End: Variable.__setitem__
    
    def __repr__(self):
        
        class_name = self.__class__.__name__
        name       = self.name(default='', long=True)
#        shape      = re.sub(",\)", ")" , str(self.shape))
        shape      = str(self.shape).replace(',)', ')')
        if shape is not None:
            return '<CF %s: %s%s>' % (class_name, name, shape)
        else:
            return '<CF %s: %s>' % (class_name, name)
    #--- End: def Variable.__repr__

    def __delattr__(self, attr):

        # Delete an attribute and remove it from the _properties set
        try:
            self.__dict__['_properties'].remove(attr)
        except (AttributeError, KeyError):
            pass
        super(Variable, self).__delattr__(attr)
    #--- End: def Variable.__delattr__

    def __setattr__(self, attr, value):

        # Set an attribute and, if it matches certain criteria, store
        # its name in the '_properties' set attribute.

        # It is important to use object.__setattr__, rather than
        # assigning to self.__dict__, so that an exception is raised
        # when writing to read only attributes.

        if not attr.startswith('_') or attr == '_FillValue':
            try:
                self.__dict__['_properties'].add(attr)
            except KeyError:
                self.__dict__['_properties'] = set((attr,))
        super(Variable, self).__setattr__(attr, value)
    #--- End: def Variable.__setattr__

    def __str__(self):

        return self.__repr__()
    #--- End: def Variable.__str__
                    
    # --------------------------------------------------------
    # Read-only attributes
    # --------------------------------------------------------
    @property
    def shape(self):
        '''

    Tuple of the data's dimension sizes.

    Returns
    -------
        A tuple

    '''
        try:
            shape = self._data.shape
        except AttributeError:
            return None

        if shape == ():
            shape = (1,)

        return shape
    #--- End: def Variable.shape

    @property
    def ndim(self):
        '''

    Number of data dimensions.

    Equivalent to the number of elements in shape tuple.

    Returns
    -------
        A non-negative integer

    '''
        try:
            return len(self.shape)
        except TypeError:
            raise TypeError("can\'t infer ndim of data object of '"+\
                                self.__class__.__name__+"'")
    #--- End: def Variable.ndim

    @property
    def size(self):
        '''

    Number of elements in the data.

    Equivalent to the product of the data's dimension sizes.

    Returns
    -------
        A non-negative integer

    '''
        try:
            return self._data.size
        except AttributeError:
            raise AttributeError("data object of '"+self.__class__.__name__+\
                                     "' has no attribute 'size'.")
    #--- End: def Variable.size

    @property
    def dtype(self):
        '''

    Data-type of the data's elements.

    Returns
    -------
        A numpy dtype object.

    '''
        try:
            return self._data.dtype
        except AttributeError:
            raise AttributeError("data object of '"+self.__class__.__name__+\
                                     "' has no attribute 'dtype'.")
    #--- End: def Variable.dtype

    @property
    def type(self):
        '''

    The type of the data object.

    Returns
    -------
        A type object, or None if the variable has no data.

     '''
        try:
            return type(self._data)
        except AttributeError:
            return None
    #--- End: def Variable.type

    @property
    def array(self):
        '''

    Create a numpy array deep copy of the data.

    If the data was stored as a file pointer then it will be changed
    in-place to be stored as a numpy array.

    Returns
    -------
        A numpy array.

    Examples
    -------- 
    >>> a = x.array
    >>> type(a)
    <type 'numpy.ndarray'>
    >>> a = x.array[[0,1]]
    >>> type(a)
    <type 'numpy.ndarray'>
    >>> a.shape
    (2,)

    '''
        self[...]
        return copy.deepcopy(self._data)
    #--- End: def Variable.array

    @property
    def varray(self):
        '''

    Create a numpy view of the data.

    If the data was stored as a file pointer then it will be changed
    in-place to be stored as a numpy array.

    Note that making changes to elements of the returned view
    changes the underlying data. Refer to `numpy.view`.
        
    Returns
    -------
        A numpy view.

    Examples
    -------- 
    >>> a = x.varray
    >>> print a
    array([0, 1, 2, 3, 4])
    >>> a[0] = 999
    >>> print x.varray[0]
    999
    >>> a = 'a_different_object'
    >>> print x.varray
    array([999, 1, 2, 3, 4])

    '''       
        self[...]
        return self._data.view()
    #--- End: def Variable.varray

    @property
    def first_datum(self):
        ''' 

    Return the first element of the data without replacing a file
    pointer with a numpy array.
        
    Returns
    -------
        The scalar value of the first element of the data.

    '''
        x = self._data[(0,) * self._data.ndim]
        try:
            # Return the first element if x is a size 1 array
            x.__iter__
            return x[0]
        except (AttributeError, IndexError):
            # Return x
            return x
    #--- End: def Variable.first_datum
    
    @property
    def last_datum(self):
        ''' 

    Return the last element of the data without replacing a file
    pointer with a numpy array.
    
    Returns
    -------
        The scalar value of the last element of the data.

    '''
        index = list(self._data.shape)
        for i in xrange(self._data.ndim):
            if index[i] == 1:
                index[i] = 0
            else:
                index[i] = -1
        x = self._data[tuple(index)] 
        try:
            # Return the first element if x is a size 1 array
            x.__iter__
            return x[0]
        except (AttributeError, IndexError):
            # Return x
            return x
     #--- End: def Variable.last_datum

    def copy(self, shallow=(), omit=()):
        '''

    Create a deep copy of the variable, but with shallow copies of
    selected attributes.

    If a deep copy of a variable's attribute raises an exception then
    a shallow copy is attempted. If the shallow copy also raises an
    exception then the variable's attribute is assigned to the
    returned copy.

    In particular, netCDF4.Variable instances are copied by
    assignment.

    Parameters
    ----------
    omit : sequence, optional
        A collection of the variable's attribute names which are not
        to be copied to the new variable. Supersedes duplicate entries
        in the `shallow` sequence.
    shallow : sequence, optional
        A collection of the variable's attribute names which are to be
        shallow copied, as opposed to deep copied. 

    Returns
    -------
        A deep copy of the variable with the exceptions of those
        attributes given by the `shallow` argument. 

    '''
        new = type(self)()

        shallow_set = set(shallow) - set(omit)
      
        for attr in shallow_set:
            try:
                value = getattr(self, attr)
            except AttributeError:
                continue
            if isinstance(value, netCDF4.Variable):
                setattr(new, attr, value)
            else:                
                try:
                    setattr(new, attr, copy.copy(value))
                except RuntimeError:
                    setattr(new, attr, value)

        for attr in set(dir(self)) - set(dir(new)) - shallow_set:
            value = getattr(self, attr)
            if isinstance(value, netCDF4.Variable):
                setattr(new, attr, value)
            else:
                try:
                    setattr(new, attr, copy.deepcopy(value))
                except RuntimeError:
                    try:
                        setattr(new, attr, copy.copy(value))
                    except RuntimeError:
                        setattr(new, attr, value)
                  
        return new
    #--- End: def Variable.copy

    def dump(self, id=None, nc=False, omit=()):
        '''

    Return a string containing a full description of the variable.

    Parameters
    ----------
    id : str, optional
        Set the common prefix of component names. If None then
        defaults to the class name.
    nc : bool, optional
        If True then include attributes whose names begin 'nc'.        
    omit : sequence, optional
        Omit the given attributes from the description.

    Returns
    -------
        A string containing the description of the variable.

    See also
    --------
    cf.dump
    
    '''
        if id is None:
            id = self.__class__.__name__

        string = ''
  
        if hasattr(self, '_data'):
            for attr in ('shape',):
                string += id+'.%s = %s\n' % (attr, getattr(self, attr))
                
            if self.ndim == 1:                        
                if self.size == 0:                        
                    string += id+'[...] = []\n'
                elif self.size == 1:                        
                    string += id+'[...] = %s\n' % self.first_datum
                else:
                    string += id+'[[0,-1]] = %s\n' % numpy.array((self.first_datum, self.last_datum))
            else:
                f = (0,)  * self.ndim
                l = (-1,) * self.ndim
                fs = ','.join([' 0'] * self.ndim)
                ls = ','.join(['-1'] * self.ndim)
                string += id+'[%s] = %s\n' % (fs, self.first_datum)
                string += id+'[%s] = %s\n' % (ls, self.last_datum)
                    
        properties = self.properties(nc=nc) - set(omit)

        special_properties = set([x for x in properties \
                                      if isinstance(getattr(self, x),
                                                        (Variable, Grid, CellMethods))])
        
        for prop in sorted(properties - set(special_properties)):
            name  = id+'.'+prop
            value = repr(getattr(self, prop))
            indent = ' ' * (len(name)+3)
            if value.startswith("'") or value.startswith('"'):
                indent += ' '
            string += '\n'+textwrap.fill(name+' = '+value, 79,
                                         subsequent_indent=indent)
        string += '\n'

        for prop in sorted(special_properties):
            try:
                string += '\n'+getattr(self, prop).dump(id=id+'.'+prop, nc=nc)
            except (AttributeError):   #, TypeError):
                string += '\n%s.%s = %s' % (id, prop, repr(getattr(self, prop)))
            
        return string
    #--- End: def Variable.dump

    def equals(self, other, rtol=None, atol=None, override=True):
        '''

    Return True if the variable is congruent to another variable in
    that they have identical data, property names (as returned by the
    `properties` method) and corresponding property values.
           
    Equality of numbers is to within a tolerance. Refer to `cf` for
    details.
   
    Arguments
    ---------
    other : variable
        The variable to compare against for equality.
                                                     
    Parameters                                                             
    ----------                                                             
    atol : None or float, optional                                         
        If None then use the default method for setting the absolute       
        tolerance for equality of real numbers (refer to `cf` for          
        details). If a float then set the absolute tolerance to this       
        value for all comparisons (refer to the `override` parameter).     
    override : bool, optional                                              
        If False then only use a float value of the `rtol` or `atol`       
        parameters if it can not be set from attributes of the objects     
        being compared.                                                    
    rtol : None or float, optional                                         
        If None then use the default method for setting the relative       
        tolerance for equality of real numbers (refer to `cf` for          
        details). If a float then set the relative tolerance to this       
        value for all comparisons (refer to the `override` parameter).     
    
    Returns
    -------
        True if the two variables are congruent, False otherwise.

    Examples
    --------    
    >>> y = x
    >>> x.equals(y)
    True
    >>> y =x()
    >>> y[...] = y.varray + 1
    >>> x.equals(y)
    False
    >>> y[...] = y.varray - 1
    >>> x.equals(y)
    True
    >>> y.standard_name += '_different'
    >>> x.equals(y)
    False


    '''
        # Check each instance's id
        if id(self) == id(other):
            return True

        # Check that each instance is the same type
        if type(self) != type(other):
            return False

        # Check that each instance has the same properties
        self_properties  = self.properties()  | set(('_data',))
        other_properties = other.properties() | set(('_data',))
        if self_properties != other_properties:
            return False
            
        # Check that each instance has the same property values (apart
        # from their data)
        for prop in self_properties - set(('_data',)):
            x = getattr(self, prop)
            y = getattr(other, prop)
            if not equals(x, y, rtol=rtol, atol=atol, override=override):
                return False

        # Check that the data are equal
        try: 
            self._data
        except AttributeError:
            pass
        else:     
            if self.shape != other.shape:
                return False
            if not numpy.all(self.__data_equals__(other, rtol=rtol, atol=atol,
                                                  override=override)):
                return False
        
        # Still here?
        return True
    #--- End: def Variable.equals

    def extract(self, exact=False, **kwargs):
        '''

    Determine whether or not the variable matches conditions on its
    phenomena.

    The variable's phenomena are its attributes and, if it has any,
    its size 1 coordinates.

    The phenomenon and its conditions are specified with **kwargs
    parameters.

    The variable matches the conditions if and only if it contains all
    of the specified phenomena and they pass all of their given
    criteria. A variable always matches no criteria.

    Parameters
    ----------
    exact : bool, optional
        If True then the remaining keyword arguments given by
        `**kwargs` are assumed to be unambiguous exact matches for a
        phenomenon names, as opposed to an unambiguous abbreviations.
    **kwargs :
        The remaining keyword arguments may be any unambiguous
        abbreviation of any phenomenon name, either an attribute name
        or the standard_name of a coordinate. This behaviour is
        modified by the `exact` keyword.
               
        A keyword argument's value may be one of the following:

        1. A number. A match requires that a numeric phenomenon equals
           the number using the variable's == operator.

        2. A string. A match requires that a string-valued phenomenon
           passes a regular expression match on the string. The string
           may contain regular expression special characters. To avoid
           ambiguities, it is assumed that the regular expression
           string matches the whole of the phenomenon string, i.e. the
           regular expression special characters ^ and $ are assumed
           if not given.

        3. A sequence of numbers or strings. A match requires that a
           numeric phenomenon equals at least one of the sequence's
           elements (as in 1.)  or a string-valued phenomenon passes a
           regular expression match for at least one string in the
           sequence (as in 2.).

        4. A `cf.Comparison` object. A match requires that the
           comparison for the phenomenon evaluates to True.

    Returns
    -------
        True if the variable matches the criteria, False otherwise.
        
    Examples
    --------
    >>> S
    <CF Space: air_temperature(19, 30, 24)>
    >>> s.standard_name
    'air_temperature'
    >>> s.extract(standard_name = 'air_temperature')
    True
    >>> s.extract(standard = '^air.*')
    True
    >>> s.extract(standard_name = lt('b'))
    True
    >>> s.extract(standard_name = outside('b', 'p'))
    True
    >>> s.extract(standard = ['.*temp.*', 'pressure'])
    True
    >>> s.extract(exact=True, standard_name = 'air_temperature')
    True

    >>> s.extract(exact=True, standard_ = 'air_temperature')
    False
    >>> s.extract(standard = ['temp', 'pressure'])
    False
    >>> s.extract(standard_name = inside('o', 'p'))
    False

        '''
        # Loop round criteria
        for name in kwargs:

            # ------------------------------------------------------
            # Find out how many phenomena match 'name'
            # ------------------------------------------------------
            count = 0
             
            # Check scalar coordinates, if this variable has them
            try:
                x = self.coord(name, exact=exact)
            except AttributeError:
                pass
            else:
                if x is not None and x.size == 1:
                    x = x.first_datum                
                    count += 1

            # Check attributes
            attr = None
            if exact:
                if name in self.properties(nc=True) - set(('grid',)):
                    attr = name
                    count += 1
            else:                    
                for prop in self.properties(nc=True) - set(('grid',)):
                    if prop.startswith(name):
                        attr = prop
                        count += 1
                        
            if count == 1:
                # Variable has exactly one phenomenon matching 'name'
                if attr is not None:
                    x = getattr(self, attr)

            elif count == 0:
                # Return False if variable has no phenomenon matching
                # 'name'
                return False

            elif count > 1:
                # Die if variable has more than one phenomenon
                # matching 'name'
                raise RuntimeError('more than one phenomena matches '+name)
    
            # ------------------------------------------------------
            # Get the phenomenon value (which may be a Comparison
            # object)
            # ------------------------------------------------------
            value = kwargs[name]

            # ------------------------------------------------------
            # See if the phenomenon conforms with the value
            # ------------------------------------------------------            
            result = False                        
            if isinstance(value, Comparison):
                if not value.evaluate(x):
                    return False
                
            if not isinstance(value, (list, tuple, numpy.ndarray)):
                values = (value,)
            else:
                values = value

            for v in values:  
                if isinstance(v, basestring):
                    # x is a string
                    if exact:
                        result = x == v
                    else:
                        result = re.search('^%s$' % v, x)
                else:
                    # x is numeric                    
                    result = equals(x, v, override=False)
                    
                if result:
                    break
                else:
                    return False
            #--- End: for v in values:

        #--- End: for name in kwargs:

        # Still here? Then select the variable
        return True
    #--- End: def Variable.extract
      
    def name(self, long=False, ncvar=False, default=None):
        ''' 

    Return the `standard_name` attribute of the variable.

    If there is no `standard_name` attribute then return one of the
    `long_name` attribute, the `ncvar` attribute or the value of the
    default parameter, depending on the values of the parameters.

    Parameters
    ----------
    long : bool, optional
        If True, return the `long_name` if `standard_name` does not
        exist.
    ncvar : bool, optional
        If True, return `ncvar` if neither `standard_name` not
        `long_name` have already been returned.
    default : None or object, optional
        Return `default` if `standard_name`, `long_name` nor `ncvar`
        have already been returned.

    Returns
    -------
        The name of the variable.

    '''
        try:
            return self.standard_name
        except AttributeError:
            pass

        if long:
            try:
                return self.long_name
            except AttributeError:
                pass
            
        if ncvar:
            try: 
                return self.ncvar
            except AttributeError: 
                pass            

        return default
    #--- End: def Variable.name
        
    def ncatts(self):
        '''

    Return a set of attributes which are netCDF attributes suitable
    for writing to a netCDF file.

    All writable attributes are considered netCDF attributes except
    those starting with an underscore, (with the exception of
    '_FillValue') and those starting with 'nc'.
    
    To create an attribute which will not be in the set of netCDF
    attributes, assign directly to the __dict__ dictionary as opposed
    to using setattr.

    Returns
    -------
        A set of attributes which are netCDF attributes.

    '''        
        return self.properties(nc=False)
    #--- End: def Variable.ncatts

    def properties(self, nc=False):
        # __doc__ is copied from _find_properties.__doc__

        return _find_properties(self, nc=nc)
    #--- End: def Variable.properties
    properties.__doc__ = _find_properties.__doc__

    def repoint(self, test=False):
        '''        

    Reset the data to a file pointer if one is available, regardless
    of of whether the data is currently stored as a numpy array or
    not. 

    This may free memory, but any changes to data stored as a numpy
    array will be lost.

    Parameters
    ----------
    test : bool, optional
        If True then do not reinstate a file pointer if it exists.
    
    Returns
    -------
        True if a file pointer exists, False otherwise.

    '''
        try:
            self._file_pointer
        except AttributeError:
            return False        

        if test:
            return True
        
        self._data = self._file_pointer           
        return True
    #--- End: def Variable.repoint

#--- End class Variable


class Coordinate(Variable):
    '''
    
    Coordinate(**kwargs)

    A CF coordinate object.

    Refer to `cf.Variable`, `cf.Grid` and the `cf` module for more
    details.

    Parameters
    ----------
    **kwargs :
        The new variable is given attributes named by the keywords,
        whose values are deep copies the keyword values.

    Attributes
    ----------   
    array : numpy array
        A numpy array deep copy of the data.
    bounds: cf.CoordinateBounds, optional
        The coordinate's cell boundaries.
    climatology: cf.CoordinateBounds, optional
        A time coordinate's climatological cell boundaries.
    compress : cf.VariableList, optional
        List of coordinates which are compressed by gathering into
        this coordinate's single dimension.
    dtype : numpy.dtype
        Describes the format of the elements in the data array (refer
        to `numpy.ndarray.dtype`).
    ncvar: string, optional 
        Contains the name of the variable in the original netCDF
        file. If present when the space is written to a netCDF file,
        then used for the output netCDF variable name.
    ndim : int
        The data array's number of dimensions (refer to
        `numpy.ndarray.ndim`).
    shape: tuple of ints
        The shape of the data array (refer to `numpy.ndarray.shape`).
    size : int
        The number of elements in the data array (refer to
        `numpy.ndarray.size`).
    transform : str, optional                
        The key of a grid's `transform` attribute which contains a
        transformation for this coordinate.
    type : type
        The type of the data object, which is either a numpy array or
        a file pointer.
    varray : numpy array
        A numpy view of the data.
    _atol : None or float, optional
        Absolute tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.
    _rtol : None or float, optional
        Relative tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.

    Methods
    -------
    copy        : Create a copy.
    dump        : Return a string containing a full description of the
                  coordinate.
    equals      : Determine whether two coordinates are congruent with
                  each other.
    extract     : Determine whether a coordinate matches phenomena
                  criteria.
    first_datum : Return the first element of the data without
                  replacing a file pointer with a numpy array.
    last_datum  : Return the last element of the data without
                  replacing a file pointer with a numpy array.
    name        : Return the name (standard_name, long_name or ncvar).
    ncatts      : Return the set of attributes which are netCDF
                  attributes.
    properties  : Return a set of writable, public attributes.
    repoint     : Revert the data to a file pointer.
 
    Refer to `cf.Variable` (the base class of Coordinate) for more
    details.

    '''
    def __getitem__(self, index):
        '''

    Implement indexing of the coordinate. Refer to `cf`.

    If the coordinate has bounds, then they are also sliced with
    [index,:].

    Returns
    -------
        A deep copy of the coordinate with the data and bounds (if
        present) resized according to the given index.

    '''           
        try:
            # Convert the data to a numpy array in place
            self = _convert_data_to_numpy(self, squeeze=False)    
            got_data = True
        except AttributeError:
            got_data = False

        try:
            self.bounds._data
            got_bounds = True
        except AttributeError:
            got_bounds = False

        try:
            self.climatology._data
            got_climatology = True
        except AttributeError:
            got_climatology = False

        # Copy the variable
        new = self.copy()

        if got_data or got_bounds or got_climatology:
            # Parse the index
            index = _parse_index(self, index)

        if got_data:
            # Slice the data in-place one dimension at a time
            new = _slice_data(new, index)
            
        if got_bounds:
            # Slice the bounds 
            bounds_index = index           
            if self.bounds.ndim == self.ndim + 1:
                bounds_index += (slice(None),)
            new.bounds = self.bounds[bounds_index]
                        
        if got_climatology:
            # Slice the climatological bounds
            bounds_index = index                       
            if self.climatology.ndim == self.ndim + 1:
                bounds_index += (slice(None),)
            new.climatology = self.climatology[bounds_index]
                        
        return new
    #--- End: Coordinate.__getitem__
    
    def dump(self, nc=False, id=None, omit=()): 
        '''

    Return a string containing a full description of the coordinate.

    Parameters
    ----------
    id : str, optional
        Set the common prefix of component names. If None then
        defaults to the class name.
    nc : bool, optional
        If True then include attributes whose names begin 'nc'.        
    omit : sequence, optional
        Omit the given attributes from the description.

    Returns
    -------
        A string containing the description of the coordinate.

    See also
    --------
    cf.dump

    '''
        if id is None:
            id = self.__class__.__name__       

        try:
            default = self.ncvar
        except AttributeError:
            default = ''

        string  = self.name(default=default, long=True) + ' coordinate\n'
        string += ''.ljust(len(string)-1, '-') + '\n'
        
        string += Variable.dump(self, id=id, nc=nc, omit=omit+('bounds', 
                                                               'climatology'))
        for attr in ('bounds', 'climatology'):
            try:
                bounds = getattr(self, attr)
            except AttributeError:
                continue

            string += '\n'
            try:
                bounds._data
            except AttributeError:
                string += id+'.'+attr+' = %s\n' % repr(bounds)
            else:
                string += id+'.'+attr+'.shape = '+ repr(bounds.shape) +'\n'
                if bounds.ndim == 1:
                    string += id+'.'+attr+'[...] = %s\n' % \
                        numpy.array((bounds.first_datum, bounds.last_datum))
                else:
                    ndim_minus1 = bounds.ndim - 1
                    f = tuple([ 0 for x in xrange(ndim_minus1)])
                    l = tuple([-1 for x in xrange(ndim_minus1)])
                    f += (slice(None),)
                    l += (slice(None),)
                    
                    fs = ','.join([' 0'] * ndim_minus1)
                    ls = ','.join(['-1'] * ndim_minus1)

                    string += id+'.'+attr+'[%s,:] = %s\n' % (fs, bounds._data[f])
                    string += id+'.'+attr+'[%s,:] = %s\n' % (ls, bounds._data[l])

        return string
    #--- End: def Coordinate.dump

    def ncatts(self):
        '''

    Return a set of attributes which are netCDF attributes suitable
    for writing to a netCDF file.

    All writable attributes are considered netCDF attributes except
    those starting with an underscore; (with the exception of
    '_FillValue'); those starting with 'nc'; and the `bounds`,
    `climatology` and `transform` attributes.
    
    To create an attribute which will not be in the set of netCDF
    attributes, assign directly to the __dict__ dictionary as opposed
    to using setattr.

    Returns
    -------
        A set of attributes which are netCDF attributes.

    '''        
        return Variable.ncatts(self) - set(('bounds', 'climatology', 
                                            'transform'))
    #--- End: def Coordinate.ncatts

    def repoint(self, test=False):
        '''        

    Reset the data of all coordinate components to a file pointers if
    they are all available, regardless of of whether the data are
    currently stored as a numpy arrays or not.

    This may free memory, but any changes to data stored as a numpy
    arrays will be lost.

    Parameters
    ----------
    test : bool, optional
        If True then do not reinstate a file pointers.
    
    Returns
    -------
        True if all file pointers of all coordinate components exist,
        False otherwise.

    '''        
        if not Variable.repoint(self, test=True):
            return False

        try: 
            if not self.bounds.repoint(test=True):
                return False
        except AttributeError:
            pass

        try: 
            if not self.climatology.repoint(test=True):
                return False
        except AttributeError:
            pass

        if test:
            return True

        Variable.repoint(self, test=False)
        try: 
            self.bounds.repoint(test=False)
        except AttributeError:
            pass
        try: 
            self.climatology.repoint(test=False)
        except AttributeError:
            pass

        return True
    #--- End: def Coordinate.repoint

#--- End: class Coordinate


class CoordinateBounds(Variable):
    '''
    
    CoordinateBounds(properties={})

    A CF coordinate bounds object.

    Refer to `cf.Variable` and the `cf` module for more details.

    Parameters
    ----------
    properties : dict, optional
        The new coordinate bounds is given attributes named by keys of
        the dictionary, whose values are deep copies the key
        values.

    Overloaded operators
    --------------------
    Refer to `cf.Variable`.

    Attributes
    ----------   
    array : numpy array
        A numpy array deep copy of the data.
    dtype : numpy.dtype
        Describes the format of the elements in the data array (refer
        to `numpy.ndarray.dtype`).
    ncvar : string, optional 
        Contains the name of the variable in the original netCDF
        file. If present when the space is written to a netCDF file,
        then used for the output netCDF variable name.
    ndim : int
        The data array's number of dimensions (refer to
        `numpy.ndarray.ndim`).
    shape : tuple of ints
        The shape of the data array (refer to `numpy.ndarray.shape`).
    size : int
        The number of elements in the data array (refer to
        `numpy.ndarray.size`).
    type : type
        The type of the data object, which is either a numpy array or
        a file pointer.
    varray : numpy array
        A numpy view of the data.
    _atol : None or float, optional
        Absolute tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.
    _rtol : None or float, optional
        Relative tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.

    Methods
    -------
    copy        : Create a copy.
    dump        : Return a string containing a full description of the
                  coordinate bounds.
    equals      : Determine whether two coordinate bounds are
                  congruent with each other.
    extract     : Determine whether coordinate bounds match phenomena
                  criteria.
    first_datum : Return the first element of the data without
                  replacing a file pointer with a numpy array.
    last_datum  : Return the last element of the data without
                  replacing a file pointer with a numpy array.
    name        : Return the name (standard_name, long_name or ncvar).
    ncatts      : Return the set of attributes which are netCDF
                  attributes.
    properties  : Return a set of writable, public attributes.
    repoint     : Revert the data to a file pointer.
 
    Refer to `cf.Variable` (the base class of CoordinateBounds) for
    more details.

    '''
    pass
#--- End: class CoordinateBounds

class CellMeasures(Variable):
    '''
    
    CellMeasures(properties={})

    A CF cell measures object.

    Refer to `cf.Variable` and the `cf` module for more details.

    Parameters
    ----------
    properties : dict, optional
        The new cell measures is given attributes named by keys of the
        dictionary, whose values are deep copies the key values.

    Overloaded operators
    --------------------
    Refer to `cf.Variable`.

    Attributes
    ----------   
    array : numpy array
        A numpy array deep copy of the data.
    dtype : numpy.dtype
        Describes the format of the elements in the data array (refer
        to `numpy.ndarray.dtype`).
    ncvar : string, optional 
        Contains the name of the variable in the original netCDF
        file. If present when the space is written to a netCDF file,
        then used for the output netCDF variable name.
    ndim : int
        The data array's number of dimensions (refer to
        `numpy.ndarray.ndim`).
    shape : tuple of ints
        The shape of the data array (refer to `numpy.ndarray.shape`).
    size : int
        The number of elements in the data array (refer to
        `numpy.ndarray.size`).
    type : type
        The type of the data object, which is either a numpy array or
        a file pointer.
    varray : numpy array
        A numpy view of the data.
    _atol : None or float, optional
        Absolute tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.
    _rtol : None or float, optional
        Relative tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.

    Methods
    -------
    copy        : Create a copy.
    dump        : Return a string containing a full description of the
                  cell measures.
    equals      : Determine whether two cell measures are congruent
                  with each other.
    extract     : Determine whether a cell measures matches phenomena
                  criteria.
    first_datum : Return the first element of the data without
                  replacing a file pointer with a numpy array.
    last_datum  : Return the last element of the data without
                  replacing a file pointer with a numpy array.
    name        : Return the name (standard_name, long_name or ncvar).
    ncatts      : Return the set of attributes which are netCDF
                  attributes.
    properties  : Return a set of writable, public attributes.
    repoint     : Revert the data to a file pointer.
 
    Refer to `cf.Variable` for more details.

    '''
    pass
#--- End: class CellMeasures

class Space(Variable):

    '''
    Space(**kwargs)

    Represent a space construct according to the CF data model.

    Space structure
    ---------------
    A space is composed as follows (ignoring attributes not composed
    from `cf` classes):

    space --+--> cell methods
            |      
            +--> ancillary variables      
            |      
            +--> grid --+--> coordinates ---> coordinate bounds
                        |
                        +--> cell measures
                        |
                        +--> transforms
   
    All components of a space are optional. Refer to the `cf` module
    for more details.

    Refer to the `cf` module for details on data access by indexing
    and data storage.

    Slicing a space instance with call arguments
    --------------------------------------------
    A space is callable and keyword arguments may be used to define
    slices of the space's data based on conditions on the space's
    coordinate values, creating a new space whose data and grid are a
    subset of the original.

      Call parameters
      ---------------
      exact : bool, optional
          Refer to `cf.Space.slice`
      **kwargs : 
          Refer to `cf.Space.slice`

    A call to a space is exactly equivalent to a call to a space's
    `slice` method, i.e. for a space, s, `s(...)` is exactly
    equivalent to `s.slice(...)`. Refer to the `slice` method for
    details and examples.
    
    Overloaded operators
    --------------------
    Refer to `cf.Variable`.

    Parameters
    ----------
    **kwargs :
        The new variable is given attributes named by the keywords,
        whose values are deep copies the keyword values.

    Attributes
    ----------   
    ancillary_variables : cf.SpaceList, optional
        Contains other spaces comprising the variables described by a
        CF ancillary_variables string.
    array : numpy array
        A numpy array deep copy of the data.
    cell_methods : cf.CellMethods, optional           
        Contains the information from a CF cell_methods string parsed
        into a data structure. Refer to `cf.CellMethods` for more
        details.
    dtype : numpy.dtype
        Describes the format of the elements in the data array (refer
        to `numpy.ndarray.dtype`).
    grid : cf.Grid, optional
        Describes the space's dimensionality and contains the space's
        dimension coordinates, auxiliary coordinates and cell
        measures. Refer to `cf.Grid` for more details.
    ncvar : string, optional 
        Contains the name of the variable in the original netCDF
        file. If present when the space is written to a netCDF file,
        then used for the output netCDF variable name.
    ndim : int
        The data array's number of dimensions (refer to
        `numpy.ndarray.ndim`).
    shape : tuple of ints
        The shape of the data array (refer to `numpy.ndarray.shape`).
    size  : int
        The number of elements in the data array (refer to
        `numpy.ndarray.size`).
    type  : type
        The type of the data object, which is either a numpy array or
        a file pointer.
    varray : numpy array
        A numpy view of the data.
    _atol : None or float, optional
        Absolute tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.
    _rtol : None or float, optional
        Relative tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.

    Methods
    -------    
    coord       : Return a coordinate identified by its name.
    copy        : Create a copy.
    dump        : Return a string containing a full description of the
                  object.
    equals      : Determine whether two spaces are congruent with each
                  other.
    extract     : Determine whether a space matches phenomena
                  criteria.
    first_datum : Return the first element of the data without
                  replacing a file pointer with a numpy array.
    last_datum  : Return the last element of the data without
                  replacing a file pointer with a numpy array.
    name        : Return the name (standard_name, long_name or ncvar).
    ncatts      : Return the set of attributes which are netCDF
                  attributes.
    properties  : Return a set of writable, public attributes.
    repoint     : Revert the data to a file pointer.
    slice       : Slice a space with conditions on coordinate values.
 
    '''

    def __getitem__(self, index):
        '''

    Implement indexing of the space. Refer to `cf`.

    If the coordinate has a grid, then its coordinates and cell
    measures are sliced and the grid's dimensionality resized.

    Returns
    -------
        A deep copy of the space with the data and grid resized
        according to the given index.

    '''     
        # Convert the data to a numpy array in place
        self = _convert_data_to_numpy(self, squeeze=True)

        # Parse the index
        index = _parse_index(self, index)

        # Copy the variable

        ## It'd be nice not to deep copy the grid coordinates and cell
        ## measures now but do this in the for loop below. Would be a
        ## bit more efficient.

        new = self.copy()

        # Slice the data in-place one dimension at a time
        new = _slice_data(new, index)

        # Remove size one dimensions
        new._data = numpy.squeeze(new._data)
        if new._data.shape is ():
            new._data = numpy.expand_dims(new._data, axis=0)
          
        # Break ancillary variables
        full_slice = (slice(None),) * self.ndim
        if index != full_slice:
            try:
                del new.ancillary_variables
            except AttributeError:
                pass
            
        try:
            new.grid
        except AttributeError:
            # Return now if the space doesn't have a grid
            return new
        
        # -----------------------------------------------------------
        # Still here? Then slice the grid
        # -----------------------------------------------------------
        i = -1
        for dim in new.grid.dimensions['data'][:]:
            
            i += 1

            # Continue if no slicing to be done on this dimension
            try:
                if index[i] == slice(None):
                    continue
            except ValueError:
                pass
            
            for key in new.grid:

                # Continue if this grid component does not span this
                # dimension
                if dim not in new.grid.dimensions[key]:
                    continue

                dice = [slice(None)] * new.grid[key].ndim
                pos = new.grid.dimensions[key].index(dim)
                dice[pos] = index[i]
                new.grid[key] = new.grid[key][tuple(dice)]

                # Update dimension size
                new.grid.dimension_sizes[dim] = new.grid[key].shape[pos]        
                                
                # Remove size 1 dimensions from the list
                if new.grid.dimension_sizes[dim] == 1:
                    new.grid.dimensions['data'].remove(dim)
            #--- End: for key
        #--- End: for dim
                                
        if index != full_slice:
            # Break transform spaces
            try:
                new.grid.transform
            except AttributeError:
                pass
            else:
                for key in new.grid.transform:
                    for term, value in new.grid.transform[key].iteritems():
                        if isinstance(value, Space): 
                            new.grid.transform[key][term] = None
            
        return new
    #--- End: Space.__getitem__

    def __str__(self):

        try:
            dim_names = self.grid.dimensions['data'][:]
        except AttributeError:
            # Space has no grid
            dim_names = self.shape
        else:     
            # Space has a grid
            for i in xrange(len(dim_names)):
                try:
                    dim_names[i] = self.grid[dim_names[i]].name(ncvar=True, 
                                                                default=dim_names[i])
                except (AttributeError, KeyError):
                    pass
            dim_names = tuple(dim_names)
        # End: try
        dim_names = str(dim_names).replace("'", "")
        dim_names = dim_names.replace(',)', ')')

        string  = 'Data            : '
        string += self.name(long=True, ncvar=True, default='') + dim_names+'\n'
        try:
            string += str(self.cell_methods)
        except AttributeError:
            # Space has no cell methods
            string += 'Cell methods    :\n'

        try:
            string += str(self.grid)
        except AttributeError:
            # Space has no grid
            string += 'Dimensions      :\n'
            string += 'Auxiliary coords:\n'
            string += 'Cell measures   :\n'
        return string
    #--- End def Space.__str__

    # --------------------------------------------------------
    # Read-only attributes
    # --------------------------------------------------------
    @property
    def shape(self):
        '''

    Tuple of the data's dimension sizes.

    If the data originates from a file, the returned data dimensions
    may differ from the dimensions of the array on disk in that size
    one dimensions are not represented in the space's data, unless the
    space's data would otherwise be a scalar, in which case the shape
    is (1,).

    Returns
    -------
        A tuple of the data's dimension sizes.

    '''
        try:
            shape = self._data.shape
        except AttributeError:
            return None
        
        if len(shape) == 1:
            return shape
        
        shape = list(shape)
        while 1 in shape:
            shape.remove(1)
            
        if shape == []:
            return (1,)
            
        return tuple(shape)
    #--- End: def Space.shape

    def dump(self, id=None, nc=False, omit=()):
        '''

    Return a string containing a full description of the space.

    Parameters
    ----------
    id : str, optional
        Set the common prefix of component names. If None then
        defaults to the class name.
    nc : bool, optional
        If True then include attributes whose names begin 'nc'.        
    omit : sequence, optional
        Omit the given attributes from the description.

    Returns
    -------
        A string containing the description of the space.

    See also
    --------
    cf.dump

    '''
        if id is None:
            id = self.__class__.__name__

        # Produce summary
        string = 'summary\n-------\n'
        string += str(self) + '\n'

        # Produce dump
        name = self.name(default='', long=True)
        string += name +'\n'
        string += ''.ljust(len(name), '-') + '\n'
        
        string += Variable.dump(self, id=id, nc=nc, omit=omit)

        return string
    #--- End: def Space.dump

    def coord(self, name, role=None, key=False, exact=False):
        '''

    Find a coordinate of the space's grid by name.

    The given `name` argument is an abbreviation for (or equal to if
    the `exact` parameter is True) its `standard_name` attribute. If
    the `key` parameter is True then return the coordinate's grid key
    name. If a coordinate does not have `standard_name` attribute,
    then its `ncvar` attribute is used.

    Note that the returned coordinate is an object identity to the
    coordinate stored in the grid so, for example, a coordinate's
    attributes may be changed in-place as follows:

    >>> s.coord('height').long_name
    AttributeError: 'Coordinate' object has no attribute 'long_name'
    >>> s.coord('hei').long_name = 'HEIGHT'
    >>> s.coord('heigh').long_name
    'HEIGHT'

    Or a deep copy may be made with the coordinate's `copy` method:

    >>> h = s.coord('height').copy()

    Arguments
    ---------
    name : str
        The string to identify a coordinate by name.

    Parameters
    ----------
    exact : bool, optional
        If True then assume that the value of the `name` argument is
        equal to exactly one coordinate's name.
    key : str, optional
        Return the grid key name instead of the coordinate.
    role : str or None, optional
        Restrict the search to coordinates of the given role. Valid
        values are 'dim' and 'aux', for dimension and auxiliary
        coordinate types respectively. If None then both types of
        coordinates will be searched.
       
    Returns
    -------
        If a coordinate has been identified, return either a
        `cf.Coordinate` instance or, if the `keys` parameter is True,
        a grid key name string. otherwise, return None.

    Examples
    --------
    >>> s.coord('lon')
    <CF Coordinate: longitude(128)>
    >>> s.coord('lon', key=True)
    'dim2'
    >>> s.coord('lonX', key=True)
    None
    >>> s.coord('lon', exact=True)
    None
    >>> s.coord('longitude', exact=True)
    <CF Coordinate: longitude(128)>

    '''
        try:
            return self.grid.coord(name, role=role, key=key, exact=exact)
        except AttributeError:
            return None
    #--- End: def Space.coord

    def ncatts(self):
        '''

    Return a set of attributes which are netCDF attributes suitable
    for writing to a netCDF file.

    All writable attributes are considered netCDF attributes except
    those starting with an underscore, (with the exception of
    '_FillValue'), those starting with 'nc' and the `grid` attribute.
    
    To create an attribute which will not be in the set of netCDF
    attributes, assign directly to the __dict__ dictionary as opposed
    to using setattr.

    Returns
    -------
        A set of attributes which are netCDF attributes.

    '''
        return Variable.ncatts(self) - set(('grid',))
    #--- End: def Space.ncatts

    def repoint(self, test=False):
        '''        

    Reset the data of all space components to a file pointers if they
    are all available, regardless of of whether the data are currently
    stored as a numpy arrays or not.

    This may free memory, but any changes to data stored as a numpy
    arrays will be lost.

    Parameters
    ----------
    test : bool, optional
        If True then do not reinstate a file pointers.
    
    Returns
    -------
        True if all file pointers of all space components exist, False
        otherwise.

    '''        
        if not Variable.repoint(self, test=True):
            return False

        if hasattr(self, 'grid'):
            for key in self.grid:
                try: 
                    if not self.grid[key].repoint(test=True):
                        return False
                except AttributeError:
                    pass

        if test:
            return True

        Variable.repoint(self, test=False)
        if hasattr(self, 'grid'):
            for key in self.grid:
                try:
                    self.grid[key].repoint(test=False)
                except AttributeError:
                    pass

        return True
    #--- End: def Space.repoint
 
    def __call__(self, exact=False, **kwargs):
        '''

    Slice a space with conditions on coordinate values.

    Return a new space whose data and grid are a subset of the
    original.

    Exactly equivalent the `slice` method. Refer to `cf.Space` and
    `cf.Space.slice` for details and examples.

    Parameters
    ----------
    exact : bool, optional
        Refer to `cf.Space.slice`.
    **kwargs :
        Refer to `cf.Space.slice`.

    Returns
    -------
        A space.

    '''       
        return self.slice(exact=exact, **kwargs)
    #--- End Space.__call__

#    def uncompress(self):
#        compressed_dims = []
#
#        for dim in self.grid.dimensions['data']:
#            try:
#                self.grid[dim].compress
#                compressed_dims.append(dim)
#            except (KeyError, AttributeError):
#                pass
#
#        new = copy.deepcopy(self)
#
#        # Return if the data are not compresed by gathering
#        if not compressed_dims:
#            return new        
#        
#        # Still here?
#        dimN = len(new.grid.dimension_sizes)
#        comN = len(new.grid.get('com'))
#
#        for com_dim in compressed_dims:
#
#            compressed_list = new.grid[com_dim].compress
#
#            com = 'com'+str(comN)
#            new.grid[com] = new.grid.pop(com_dim)
#            new.grid[com].compress = []
#
#            new.grid.dimensions['data'].remove(com_dim)
#            
#            new_dim_sizes = ()
#      
#            for unc in compressed_list:
#                dim = 'dim'+str(dimN)
#                new.grid[com].compress.append(dim)
#                new.grid[dim] = new.grid.pop(unc)
#
#                new.grid.dimensions.pop(unc)
#                new.grid.dimensions[dim]      = dim
#                new.grid.dimension_sizes[dim] = new.grid.dimension_sizes.pop(unc)
#               
#                new_dim_sizes += (new.grid.dimension_sizes[dim],)
#
#                dimN  += 1                
#
#            
#            if new.ndim == 1:
#                new._data = numpy.expand_dims(new.varray, 1)
#                size  = 1
#                shape = ()
#            else:
#                new._data = numpy.swapaxes(new.varray, pos, 0)
#                size  = numpy.prod(shape)
#                shape = new.shape[1:]
#                new._data = numpy.reshape(new._data, (new._data.shape[0], size))
#
#            uncompressed = numpy.ma.masked_all((numpy.prod(new_dim_sizes), size),
#                                               dtype=new.dtype)
#            try:
#                uncompressed.fill_value = new._FillValue
#                uncompressed.fill_value = new.missing_value
#            except AttribueError:
#                pass
##            uncompressed = numpy.empty(numpy.prod(new_dim_sizes), size)
##            uncompressed = uncompressed.fill(new._FillValue)
#
#            for i in xrange(new._data.shape[0]):
#                uncompressed[new.grid[com][i],:] = new._data[i,:]
#
#            uncompressed = numpy.swapaxes(uncompressed, 0, 1)
#            uncompressed = numpy.reshape(uncompressed, shape+new_dim_sizes)
#            
#            new._data = uncompressed
#
#            comN += 1
#        #--- End: for dim in compressed_dims:
#
#        return new
#    #--- End: def Space.uncompress


    def slice(self, exact=False, **kwargs):
        '''

    Return a new space whose data and grid are a subset of the
    original by slicing the space where conditions on coordinate data
    values are met.

    A coordinate and the conditions on its data are specified with
    **kwargs parameters.

    If a coordinate is not specified, then its dimension is returned
    unchanged. A call with no parameters (kwargs = {}) is returns a
    deep copy of the space.

    The order in which coordinates are given in the parameter list is
    irrelevant.

    Unless the `exact` keyword is True, the coordinates identified in
    different space elements may vary. For example, the keyword 'lon'
    could identify the 'longitude' coordinate in one variable and
    'longwave' in another.

    Parameters
    ---------- 
    exact : bool, optional
        If True then the remaining keyword arguments given by
        `**kwargs` are assumed to be unambiguous exact matches for a
        coordinate names, as opposed to an unambiguous abbreviations.
    **kwargs :
        The keyword arguments to the call may be any unambiguous
        abbreviation of the standard_name of any one dimensional
        dimension or auxiliary coordinate which shares its dimension
        with the space's data. This behaviour is modified by the
        `exact` keyword.
              
        A keyword argument's value may be one of the following:

        1. A scalar. The dimension is sliced where coordinate equals
           (using the == operator) the scalar value.

        2. A sequence. The dimension is sliced where the coordinate
           equals (using the == operator) any of the sequence's
           elements.

        3. A `cf.Comparison` object (as, for example, returned by the
           `cf.lt` function). The dimension is sliced where the
           comparison for the coordinate evaluates to True.

        4. An array of booleans. The dimension is sliced where
           elements of this array are True.

    Returns
    -------
        A space.

    See also
    --------
    __call__

    Examples
    --------
    >>> from cf import *
    >>> s = read1('surface_air_temperature.nc')
    >>> print s
    Data            : air_temperature(time, latitude, longitude)
    Cell methods    : time: mean (interval: 1.0 month)
    Dimensions      : time(7070) -> 450-11-16 00:00:00 to 1049-12-16 12:00:00
                    : latitude(64) -> -87.8638 to 87.8638 degrees_north
                    : longitude(128) -> 0 to 357.1875 degrees_east
                    : height(1) -> 2 m
    Auxiliary coords:

    >>> s
    <CF Space: air_temperature(7070, 64, 128)>
    >>> s.slice(lon = 0)
    <CF Space: air_temperature(7070, 64)>
    >>> s.slice(lon = [0, 45, 45.1])
    <CF Space: air_temperature(7070, 64, 2)>
    >>> s.slice(lon = numpy.arange(128)*2.8125)
    <CF Space: air_temperature(7070, 64, 128)>
    >>> s.slice(lon = s.coord('lon')<90)
    <CF Space: air_temperature(7070, 64, 64)>
    >>> s.slice(lon = 0, lat = gt(0))
    <CF Space: air_temperature(7070, 32)>
    >>> s.slice(longitude = 0, exact=True)
    <CF Space: air_temperature(7070, 32)>
    >>> s.slice(lon = 0, exact=True)
    RuntimeError: Can not find dimension from keyword: lon

    The next two commands are equivalent:

    >>> s.slice(lon = lt(180))
    <CF Space: air_temperature(7070, 64, 64)>
    >>> s.slice(long = s.coord('lon')<180)
    <CF Space: air_temperature(7070, 64, 64)>

    The next two commands are equivalent:

    >>> s.slice(lon = inside(90,135))
    <CF Space: air_temperature(7070, 64, 17)>
    >>> lon = s.coord('lon') ; s.slice(lon = (lon>=90) & (lon<=135))
    <CF Space: air_temperature(7070, 64, 17)>

    The next three commands are equivalent:

    >>> s.slice()
    >>> s.slice(**{})
    >>> copy.deepcopy(s)

    '''       
        if not kwargs:
            return copy.deepcopy(self)

        dimensions       = []
        dimension_slices = []

        # Initialize index
        index  = [slice(None)] * self.ndim
        index0 = index[:]

        # Loop round slice criteria
        for name in kwargs:
                                    
            key = self.coord(name, key=True, exact=exact)

            if key is None:
                raise RuntimeError('Can not find dimension from keyword: '+name)
            if key.startswith('dim'):
                dim = key
            else:
                aux = key
                dim = self.grid.dimensions[aux][0]
                if self.grid[aux].ndim > 1:
                    raise RuntimeError('Can not define a slice for a multidimensional coordinate: '+name)
            if dim in dimensions:
                raise RuntimeError('More than one slice for dimension '+name)
            dimensions.append(dim)

            # Define x, the coordinate for the specified dimension
            try:
                x = self.grid[key]
            except KeyError:
                raise RuntimeError('Hocus Pocus')
            
            value = kwargs[name]

            slice_dim = []
            if isinstance(value, Comparison):
                slice_dim.extend(numpy.where(value.evaluate(x))[0])

            elif isinstance(value, numpy.ndarray) and value.dtype.char == '?':
                # Value is a numpy boolean array
                slice_dim.extend(numpy.where(value)[0])

            else:
                values = value 
                if not isinstance(values, (list, tuple, numpy.ndarray)):
                    values = [value]
                for v in values:
                    mask = x == float(v)
                    slice_dim.extend(numpy.where(mask)[0])
            
            new_dim_size = len(slice_dim)
            if new_dim_size == self.grid.dimension_sizes[dim]:
                slice_dim = slice(None)
            elif new_dim_size == 0:
                raise IndexError('No indices selected for dimension '+name)

            # Add slice_dim to the right place in the data slice
            pos        = self.grid.dimensions['data'].index(dim)
            index[pos] = slice_dim        
            
        #--- End for criterion

        return self[tuple(index)]
    #--- End Space.slice

#--- End: class Space


class CfList(collections.MutableSequence):
    '''

    CfList(sequence=())

    A list-like object (a 'cf list') suitable for CF structures.
 
    When requesting a non-private attribute that the cf list does not
    have, and the `_elements_atts` attribute is True, instead of
    raising an AttributeError a cf list is returned containing the
    values of the attribute from each of the elements, with a value of
    None if an element does not have the attribute. The setting of an
    attribute is as usual, i.e. it is set on the object itself rather
    that the object's elements.

    Parameters
    ----------
    sequence : iterable, optional
        Initialize new list from sequence's items.

    Overloaded operators
    --------------------
    The `in` (set membership), == and != operators are overloaded to
    use numerically tolerant equality.

    The +, *, += and *= operators behave as for a built-in list. Note
    in particular that in-place changes to an element of the result of
    one of these operations will affect the equivalent element on the
    right hand side, and possibly other equivalent elements in the
    result itself.
 
    Attributes
    ----------
    _atol : None or float, optional
        Absolute tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.
    _elements_atts : bool
        If True then do not raise an exception when requesting a
        non-existent attribute, but broadcast the request to each
        element of the list.
    _rtol : None or float, optional
        Relative tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.

    Methods
    -------
    append     : As for a built-in list.
    copy       : Create a deep copy.
    count      : As for a built-in list but using numerically tolerant
                 equality.
    equals     : Determine whether two lists are congruent element-wise.
    extend     : As for a built-in list.
    index      : As for a built-in list but using numerically tolerant
                 equality.
    insert     : As for a built-in list.
    pop        : As for a built-in list.
    properties : Return a set of writable, public attributes.
    remove     : As for a built-in list.
    reverse    : As for a built-in list.
    sort       : As for a built-in list.

    '''
    def __init__(self, sequence=()):

        # return elements' attributes if not set an attribute is not
        # defined on self
        self._elements_atts = True

        # Initialize self._dict, bypassing setattr so that '_list'
        # does not end up in the self._properties set
        self._list = list(sequence)

        # Note: the extend works because s.extend(x) is the as
        # s[len(s):len(s)]=x and we have defined __setitem__ to work
        # on s._list    
    #--- End: def CfList.__init__

    def __repr__(self):
        
        return repr(self._list)
    #--- End: def CfList.__repr__

    def __setattr__(self, attr, value):
        
        # See Variable.__setattr__ for notes.
        if not attr.startswith('_'):
            try:
                self.__dict__['_properties'].add(attr)
            except KeyError:
                self.__dict__['_properties'] = set((attr,))
        super(CfList, self).__setattr__(attr, value)
    #--- End: def CfList.__setattr__

    def __delattr__(self, attr):

        # Delete an attribute and remove it from the _properties set
        try:
            self.__dict__['_properties'].remove(attr)
        except (AttributeError, KeyError):
            pass
        super(CfList, self).__delattr__(attr)
    #--- End: def CfList.__delattr__

    def __getattribute__(self, attr):
        '''

    When requesting an attribute that the instance does not have, and
    the `_elements_atts` attribute is True, instead of raising an
    AttributeError a cf list is returned of the values of the
    attribute from each of the elements, with a value of None if an
    element does not have the attribute.
   
    Returns
    -------
        A built-in list.

    '''
        try:
            # Return an attribute of the list object
            return super(CfList, self).__getattribute__(attr)
        except AttributeError:
            if attr.startswith('__'):
                return super(CfList, self).__getattribute__(attr)

            # Return a built-in list of the attributes from each
            # element of the cf list
            attrs = CfList()
            for variable in self:
                try:
                    attrs.append(getattr(variable, attr))
                except AttributeError:
                    attrs.append(None) 
            return attrs
    #--- End: def CfList.__getattribute__

    def __contains__(self, item):
        '''

        Test for set membership using numerically tolerant equality.

        '''
        try:
            rtol = self._rtol
        except AttributeError:
            rtol = None
        try:
            atol = self._atol
        except AttributeError:
            atol = None

        for s in self:
            if equals(item, s, rtol=rtol, atol=atol, override=False):
                return True
                
        return False
    #--- End: def CfList.__contains__

    def __len__(self):

        return len(self._list)
    #--- End: def CfList.__len__

    def __delitem__(self, index):

        del self._list[index]
    #--- End: def CfList.__delitem__

    def __getitem__(self, index):
        '''

    Implement indexing of the cf list.

    Returns
    -------
        A cf list which is a shallow copy of a subset of the original
        cf list.

    '''    
        subset = self._list[index]
        if isinstance(subset, list):
            return type(self)(subset)
        else:
            return subset
    #--- End: def CfList.__getitem__

    def __setitem__(self, index, value):

        self._list[index] = value
    #--- End: def CfList.__setitem__

    def __copy__(self):
        '''

    Used if copy.copy is called on the list.
    
    '''     
        return self[:]
    #--- End: def CfList.__copy__

    def __deepcopy__(self, memo):
        '''

    Used if copy.deepcopy is called on the list.
    
    See also
    --------
    copy

    ''' 
        return self.copy()
    #--- End: def CfList.__deepcopy__

    def __eq__(self, other):
        '''

    Return True if two instances are congruent in that each pair of
    their elements are equal.
            
    Equality of numbers is to within a tolerance. Refer to `equals`
    for details.

    '''
        return self.equals(other)
    #--- End: def CfList.__eq__

    def __ne__(self, other):
        '''

    Return True if two instances are not congruent in that each pair
    of their elements are not equal.
            
    Equality of numbers is to within a tolerance. Refer to `equals`
    for details.

    '''

        return not self.equals(other)
    #--- End: def CfList.__ne__    

    def __add__(self, other):
        '''

     Implement the + binary operation make to concatenate two lists.
     
     Returns
     -------
        A cf list.
 
        '''
        new = copy.deepcopy(self)
        new.extend(other)
        return new
    #--- End def CfList.__add__

    def __mul__(self, other):
        '''

     Implement the * binary operation make concatenated shallow
     copies of the list.
     
     The right hand side of the operation must be an integer.

     Returns
     -------
        A cf list.
        '''
        if not isinstance(other, int):
            raise TypeError("can't multiply cf list by non-int of type '"+ \
                                other.__class__.__name__+"'")
        
        new = type(self)()
        
        if other <= 0:
            # Return empty list for non-positive integer
            return new
        
        for i in xrange(other):
            new.extend(self)
            
        return new
    #--- End def CfList.__mul__

    def __rmul__(self, other):
        '''

     Implement the * binary operation make concatenated shallow
     copies of the list.
     
     The left hand side of the operation must be an integer.

     Returns
     -------
         A cf list.
     '''
        return self.__mul__(other)
    #--- End def CfList.__rmul__

    def __iadd__(self, other):
        '''

     Implement the += augmented assignment make to extend the list in
     place.
     
     Returns
     -------
        A cf list.
        '''
        self.extend(other[:])
        return self
    #--- End def CfList.__iadd__
        
    def __imul__(self, other):
        '''

     Implement the *= augmented assignment make concatenated shallow
     copies of the list in place.
     
     The right hand side of the operation must be an integer.

     Returns
     -------
         A cf list.
     '''
        self = self.__mul__(other)
        return self
    #--- End def CfList.__imul__

    def count(self, value):
        '''

    Return number of occurrences of value using numerically tolerant
    equality.

    '''
        try:
            rtol = self._rtol
        except AttributeError:
            rtol = None
        try:
            atol = self._atol
        except AttributeError:
            atol = None

        return sum(1 for v in self if equals(value, v, rtol=rtol,
                                             atol=atol,  override=False))
    #--- End def CfList.count

    def index(self, value, start=0, stop=None):
        '''

    Return first index of value using numerically tolerant
    equality. Restrict the search to the slice 'start:stop'. If `stop`
    is None then the slice is 'start:'.

    '''
        try:
            rtol = self._rtol
        except AttributeError:
            rtol = None
        try:
            atol = self._atol
        except AttributeError:
            atol = None

        if start < 0:
            start = len(self) + start
        if stop is None:
            stop = len(self)
        elif stop < 0:
            stop = len(self) + stop

	for i, v in enumerate(self):  
            if i < start or i >= stop:
                continue
            if equals(value, v, rtol=rtol, atol=atol, override=False):
                return i
	raise ValueError(self.__class__.__name__+'.index(x): x not in list')
    #--- End def CfList.index

    def insert(self, index, object):
        '''

    L.insert(index, object) -- insert object before index

    '''
        self._list.insert(index, object)
    #--- End: def CfList.insert

    def copy(self):
        '''

    Create a deep copy of the list. Equivalent to calling
    copy.deepcopy on the list.

    Returns
    -------
        A deep copy of the list.
    '''
        new = type(self)()
    
        for x in self:
            new.append(copy.deepcopy(x))

        return new
    #--- End: def CfList.copy

    def equals(self, other, rtol=None, atol=None, override=True):
        '''

    Return True if two instances are congruent in that each pair of
    their elements are equal.
            
    Equality of numbers is to within a tolerance. Refer to `cf` for
    details.

    Arguments
    ---------
    other : list or CfList
        The list to compare against.

    Parameters
    ----------
    atol : None or float, optional
        If None then use the default method for setting the absolute
        tolerance for equality of real numbers (refer to `cf` for
        details). If a float then set the absolute tolerance to this
        value for all comparisons (refer to the `override` parameter).
    override : bool, optional
        If False then only use a float value of the `rtol` or `atol`
        parameters if it can not be set from attributes of the objects
        being compared.
    rtol : None or float, optional
        If None then use the default method for setting the relative
        tolerance for equality of real numbers (refer to `cf` for
        details). If a float then set the relative tolerance to this
        value for all comparisons (refer to the `override` parameter).     
   
    Returns
    -------
        True if the two instances are congruent, False otherwise.

    '''
        if id(self) == id(other):
            return True
         
        # Check that each instance is the same type
        if type(self) != type(other):
            return False

        # Check that the lists have the same number of elements
        if len(self) != len(other):
            return False
          
        # Check properties
        self_properties = self.properties()
        if self_properties != other.properties():
            return False
        
        for prop in self_properties: 
            x = getattr(self, prop)
            y = getattr(other, prop)
            if not equals(x, y, rtol=rtol, atol=atol, override=override):
                return False

        # Check the element values
        for i in xrange(len(self)):
            if not equals(self[i], other[i], 
                          rtol=rtol, atol=atol, override=override):
                return False

        return True
    #--- End: def CfList.equals

    def properties(self, nc=False):
        # __doc__ is copied from _find_properties.__doc__
        
        return _find_properties(self, nc=nc)
    #--- End: def CfList.properties
    properties.__doc__ = _find_properties.__doc__
    
#--- End: class CfList            


class VariableList(CfList):
    '''
    
    VariableList(sequence=())

    A modified list for variables (any object which is subclass of
    Variable, such as a space, coordinate, etc.) supporting
    element-wise attribute retrieval and subsetting by phenomena
    criteria.

    When requesting a non-private attribute that the cf list does not
    have, and the `_elements_atts` attribute is True, instead of
    raising an AttributeError a cf list is returned containing the
    values of the attribute from each of the elements, with a value of
    None if an element does not have the attribute. The setting of an
    attribute is as usual, i.e. it is set on the object itself rather
    that the object's elements.

    Parameters
    ----------
    sequence : iterable, optional
        Initialize new list from sequence's items.

    Overloaded operators
    --------------------
    Refer to `cf.CfList`.

    Attributes
    ----------
    _atol : None or float, optional
        Absolute tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.
    _elements_atts : bool
        If True then do not raise an exception when requesting a
        non-existent attribute, but broadcast the request to each
        element of the list.
    _rtol : None or float, optional
        Relative tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.

    Methods
    -------
    append     : As for a built-in list. 
    copy       : Create a deep copy.
    count      : As for a built-in list but using numerically tolerant
                 equality.
    dump       : Return a string containing a full description of the
                 each of the list's elements.
    equals     : Determine whether two lists are congruent
                 element-wise.
    extend     : As for a built-in list.
    extract    : Select list elements which match phenomena criteria. 
    index      : As for a built-in list but using numerically tolerant
                 equality.
    insert     : As for a built-in list.
    pop        : As for a built-in list.
    properties : Return a set of writable, public attributes.
    remove     : As for a built-in list.
    reverse    : As for a built-in list.
    sort       : As for a built-in list.

    '''

    def __repr__(self):

        string = []
        for x in self:
            string.append(repr(x))
        return '[' + ',\n '.join(string) + ']'
    #--- End: def VariableList.__repr__

    def __str__(self):

        string=[]
        for x in self:
            string.append(str(x))
        return '\n'.join(string)
    #--- End: def VariableList.__str__

    def __lt__(self, other):

        raise NotImplementedError("the < operator can\'t be used on a '"+\
                                      self.__class__.__name__+"' object")
    #--- End: def VariableList.__lt__

    def __le__(self, other):

        raise NotImplementedError("the <= operator can\'t be used on a '"+\
                                      self.__class__.__name__+"' object")
    #--- End: def VariableList.__lt__

    def __gt__(self, other):

        raise NotImplementedError("the > operator can\'t be used on a '"+\
                                      self.__class__.__name__+"' object")
    #--- End: def VariableList.__lt__

    def __ge__(self, other):

        raise NotImplementedError("the >= operator can\'t be used on a '"+\
                                      self.__class__.__name__+"' object")
    #--- End: def VariableList.__lt__

    def dump(self, id=None, nc=False, omit=()):
        '''

    Return a string containing a full description of each variable in
    the list, utilizing each variable's `dump` method.

    Parameters
    ----------
    id : str, optional
        Set the common prefix of variable component names. If None
        then defaults to the class name.
    nc : bool, optional
        If True then include attributes whose names begin 'nc'.        
    omit : sequence, optional
        Omit the given attributes from each variable's the
        description.

    Returns
    -------
        A string containing the descriptions of each variable in the
        list.

    See also
    --------
    cf.dump

    '''
        string=[]
        for x in self:
            string.append(x.dump(id=id, nc=nc, omit=omit))
        return '\n'.join(string)
    #--- End: def VariableList.dump

    def extract(self, deep=False, exact=False, **kwargs):
        ''' 

    Return a subset of a list of variables by selecting only the
    variables which match conditions on their phenomena.

    A variable's phenomena are its attributes and, if it has any, its
    size 1 coordinates.

    A phenomenon and its conditions are specified with **kwargs
    parameters.

    A variable in the list matches the conditions if and only if it
    contains all of the specified phenomena and they pass all of their
    given criteria. A variable always matches no criteria (kwargs={})

    A match for a variable is determined by passing the criteria to
    the variable's `extract` method.

    Unless the `exact` keyword is True, the phenomena identified in
    different variable elements may vary. For example, the keyword
    'unit' could identify the 'units' phenomenon in one variable and
    'unitary' in another.

    By default, the returned list of variables is a shallow copy of
    part of the original list (in the same way that a standard slice
    is). To return a list whose elements are deep copies, set the
    `deep` parameter to True.
    
    Parameters
    ----------
    deep : bool, optional 
        If True then return deep copies of variables in the list.
    exact : bool, optional
        If True then the remaining keyword arguments given by
        `**kwargs` are assumed to be unambiguous exact matches for a
        phenomenon names, as opposed to an unambiguous abbreviations.
    **kwargs :
        Other keyword arguments giving any phenomena criteria are
        passed as **kwargs to the `extract` method of each of the
        list's variables. Refer to `cf.Variable.extract`.
    
    Returns
    -------
        A list of variables

    Examples
    --------
    >>> s
    [<CF Space: pressure(30, 24)>,
     <CF Space: u_compnt_of_wind(19, 29, 24)>
    >>> s.units
    ['hPa', 'm s-1']

    >>> s.extract(units = 'm s-1')
    [<CF Space: u_compnt_of_wind(19, 29, 24)>]

    >>> t = s.extract(uni = 'm\s*s-1')
    >>> t
    [<CF Space: u_compnt_of_wind(19, 29, 24)>]
    >>> t[0] is s[1]
    True
    >>> t = s.extract(uni = 'm\s*s-1', deep=True)
    >>> t[0] is s[1]
    False
   
    Refer to `cf.Variable.extract` for more examples.

    '''
        # Define a new instance
        new = type(self)()

        for variable in self:
            if variable.extract(exact=exact, **kwargs):
                if deep:
                    new.append(copy.deepcopy(variable))
                else:
                    new.append(variable)

        return new
    #--- End: def VariableList.extract

#--- End: class VariableList            


class SpaceList(VariableList):
    '''
    
    SpaceList(sequence=())

    A modified list for spaces supporting element-wise slicing,
    element-wise attribute retrieval and subsetting by phenomena
    criteria.


    When requesting a non-private attribute that the cf list does not
    have, and the `_elements_atts` attribute is True, instead of
    raising an AttributeError a cf list is returned containing the
    values of the attribute from each of the elements, with a value of
    None if an element does not have the attribute. The setting of an
    attribute is as usual, i.e. it is set on the object itself rather
    that the object's elements.

    Slicing the space list returns a new space list with each element
    sliced with its `slice` method.

    Parameters
    ----------
    sequence : iterable, optional
        Initialize new list from sequence's items.

    Overloaded operators
    --------------------
    Refer to `cf.VariableList`.

    Methods
    -------
    append     : As for a built-in list.
    copy       : Create a deep copy.
    count      : As for a built-in list but using numerically tolerant
                 equality.
    dump       : Return a string containing a full description of the
                 each of the list's elements.
    equals     : Determine whether two lists are congruent
                 element-wise.
    extend     : As for a built-in list.
    extract    : Select list elements which match phenomena criteria. 
    index      : As for a built-in list but using numerically tolerant
                 equality.
    insert     : As for a built-in list.
    pop        : As for a built-in list.
    properties : Return a set of writable, public attributes.
    remove     : As for a built-in list.
    reverse    : As for a built-in list.
    sort       : As for a built-in list.

    Examples
    --------
    >>> from cf import *
    >>> type(s)
    <class 'cf.space.SpaceList'>
    >>> s
    [<CF Space: pmsl(30, 24)>,
     <CF Space: temperature(17, 30, 24)>]
    >>> s[slice(0,2)]
    [<CF Space: pmsl(30, 24)>,
     <CF Space: temperature(17, 30, 24)>]
    >>> s[0]
    <CF Space: pmsl(30, 24)>
    >>> type(s[0])
    <class 'cf.space.Space'>

    >>> s[0].units
    'Pa'
    >>> s[-1].units
    'K'
    >>> s.units
    ['Pa', 'K']   
    >>> s.not_an_attribute
    [None, None]   

    >>> s.extract(units = 'K')
    [<CF Space: temperature(17, 30, 24)>]

    >>> s.slice(lon = le(355))
    [<CF Space: pmsl(30, 2)>,
     <CF Space: temperature(17, 30, 2)>]

    '''
    def __call__(self, exact=False, **kwargs): 
        '''         

    Return a new list of spaces in which each space element has been
    sliced according to conditions on coordinate data values.

    Exactly equivalent to calling the `slice` method. Refer to
    `cf.SpaceList.slice`.

    Parameters
    ----------
    exact : bool, optional
        Refer to `cf.SpaceList.slice`.
    **kwargs :
        Refer to `cf.SpaceList.slice`.
        
    Returns
    -------        
        A space list.
       
       '''
        return self.slice(exact=exact, **kwargs)
    #--- End: def SpaceList.__call__

    def slice(self, exact=False, **kwargs):
        ''' 

    Return a new list of spaces in which each space element has been
    sliced according to conditions on coordinate data values. Refer to
    `cf.Space.slice`.

    A coordinate and the conditions on its data are specified with
    **kwargs parameters.

    If a coordinate is not specified, then its dimension is returned
    unchanged in each space. A call with no parameters (kwargs={})
    returns a deep copy of the list of spaces.

    The order in which coordinates are given in the parameter list is
    irrelevant.

    Unless the `exact` keyword is True, the coordinates identified in
    different space elements may vary. For example, the keyword 'lon'
    could identify the 'longitude' coordinate in one variable and
    'longwave' in another.

    Parameters
    ----------
    exact : bool, optional
        If True then the remaining keyword arguments given by
        `**kwargs` are assumed to be unambiguous exact matches for
        coordinate names, as opposed to an unambiguous abbreviations.
    **kwargs :
        Other keyword arguments giving slice criteria are passed as
        **kwargs to each of the list's variables' `slice`
        methods. Refer to `cf.Space.slice`.
    
    Returns
    -------
        A space list.

    Examples
    --------
    >>>  print s
    Data            : pressure(latitude_30, longitude)
    Cell methods    : time: mean
    Dimensions      : time(1) -> 1960-01-16 00:00:00
                    : latitude_30(30) -> -5.28 to 7.48 degrees_north
                    : longitude(24) -> 354.28 to 364.4 degrees_east
    Auxiliary coords:
    
    Data            : u_compnt_of_wind(hybrid_sigma_pressure, latitude_29, longitude)
    Cell methods    : time: mean
    Dimensions      : time(1) -> 1960-01-16 00:00:00
                    : hybrid_sigma_pressure(19) -> 0.9970123 to 0.00460588 1
                    : latitude_29(29) -> -5.06 to 7.26 degrees_north
                    : longitude(24) -> 354.28 to 364.4 degrees_east
    Auxiliary coords:

    >>> s
    [<CF Space: pressure(30, 24)>,
     <CF Space: u_compnt_of_wind(19, 29, 24)>,

    >>> s.slice(long = ge(360))
    [<CF Space: pressure(30, 11)>,
     <CF Space: u_compnt_of_wind(19, 29, 11)>]

    Refer to `cf.Space.slice` for more examples.

    '''
        new = type(self)()
        for s in self:
            new.append(s.slice(exact=exact, **kwargs))

        return new
    #--- End: def SpaceList.slice

#--- End: class SpaceList            


class CfDict(collections.MutableMapping):
    '''

    CfDict(*args, **kwargs)

    A dictionary-like object (a 'cf dictionary') suitable for storing
    CF structures.

    Parameters
    ----------
    *args, **kwargs :
        Keys and values are initialized exactly as for a built-in
        dict.

    Attributes
    ----------
    _atol : None or float, optional
        Absolute tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.
    _rtol : None or float, optional
        Relative tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.

    Methods
    -------
    clear      : As for a built-in dict.
    copy       : Create a deep copy.
    equals     : Determine whether two instances are congruent with
                 each other.
    get        : As for a built-in dict.
    get_keys   : Return grid keys which match a pattern.
    has_key    : As for a built-in dict but using numerically tolerant
                 equality.
    items      : As for a built-in dict.
    iteritems  : As for a built-in dict.
    iterkeys   : As for a built-in dict.
    itervalues : As for a built-in dict.
    keys       : As for a built-in dict.
    pop        : As for a built-in dict.
    popitem    : As for a built-in dict.
    properties : Return a set of writable, public attributes.
    setdefault : As for a built-in dict.
    update     : As for a built-in dict.
    values     : As for a built-in dict.

    Overloaded operators
    --------------------
    The `in` (set membership) operator is overloaded to use
    numerically tolerant equality.

    '''
    def __init__(self, *args, **kwargs):

        self._dict = dict(*args, **kwargs)
    #--- End: def CfDict.__init__

    def __repr__(self):
        
        return repr(self._dict)
    #--- End: def CfDict.__repr__

    def __getitem__(self, key):
        '''

    Implement indexing of the cf dictionary.

    Returns
    -------
        A cf dictionary key's value.

    '''     
        return self._dict[key]
    #--- End: def CfDict.__getitem__

    def __setitem__(self, key, value):

        self._dict[key] = value
    #--- End: def CfDict.__setitem__

    def __delitem__(self, key):

        del self._dict[key]
    #--- End: def CfDict.__delitem__

    def __iter__(self):

        return iter(self._dict)
    #--- End: def CfDict.__iter__

    def __len__(self):

        return len(self._dict)
    #--- End: def CfDict.__len__

    def __contains__(self, item):
        '''

        Test for set membership using numerically tolerant equality.

        '''
        try:
            rtol = self._rtol
        except AttributeError:
            rtol = None
        try:
            atol = self._atol
        except AttributeError:
            atol = None

        for s in self:
            if equals(item, s, rtol=rtol, atol=atol, override=False):
                return True

        return False
    #--- End: def CfDict.__contains__
    
    def __setattr__(self, attr, value):
        
        # See Variable.__setattr__ for notes.
        if not attr.startswith('_'):
            try:
                self.__dict__['_properties'].add(attr)
            except KeyError:
                self.__dict__['_properties'] = set((attr,))
        super(CfDict, self).__setattr__(attr, value)
    #--- End: def CfDict.__setattr__

    def has_key(self, key):
        '''

    Return True if the key exists, using numerically tolerant
    equality.

    '''
        try:
            rtol = self._rtol
        except AttributeError:
            rtol = None
        try:
            atol = self._atol
        except AttributeError:
            atol = None

        for x in self.keys():
            if equals(key, x, rtol=rtol, atol=atol, override=False):
                return True
                
        return False
    #--- End: def CfDict.has_key

    def equals(self, other, rtol=None, atol=None, override=True):
        '''

    Return True if two instances are congruent in that they have the
    equal sets of keys and for each key, the two values are equal.
        
    Equality of numbers is to within a tolerance. Refer to `cf` for
    details.

    Arguments
    ---------
    other : dict
        The dictionary to compare against.
                                                                         
    Parameters                                                             
    ----------                                                             
    atol : None or float, optional                                         
        If None then use the default method for setting the absolute       
        tolerance for equality of real numbers (refer to `cf` for          
        details). If a float then set the absolute tolerance to this       
        value for all comparisons (refer to the `override` parameter).     
    override : bool, optional                                              
        If False then only use a float value of the `rtol` or `atol`       
        parameters if it can not be set from attributes of the objects     
        being compared.                                                    
    rtol : None or float, optional                                         
        If None then use the default method for setting the relative       
        tolerance for equality of real numbers (refer to `cf` for          
        details). If a float then set the relative tolerance to this       
        value for all comparisons (refer to the `override` parameter).     
  
    Returns
    -------
        True if the two instances are congruent, False otherwise.

    '''
        if id(self) == id(other):
            return True
        
        # Check that each instance is the same type
        if type(self) != type(other):
            return False

        # Check properties
        self_properties = self.properties()
        if self_properties != other.properties():
            return False
        
        for prop in self_properties: 
            x = getattr(self, prop)
            y = getattr(other, prop)
            if not equals(x, y, rtol=rtol, atol=atol, override=override):
                return False

        # Check that the keys are equal
        if set(self.keys()) != set(other.keys()):
            return False
            
        # Check that the key values are equal
        for key in self:
            if not equals(self[key], other[key], rtol=rtol, atol=atol,
                          override=override):
                return False
                
        # Still here?
        return True
    #--- End: def CfDict.equals

    def get_keys(self, regex):
        '''

    Return a list of the cf dictionary's key names which, at their
    beginning, match the given regular expression.
    
    Arguments
    ---------
    regex : str
        The regular expression with which to identify key names. A
        leading '^' special character is assumed if not given.

    Returns
    -------
        A list of keys names.

    Examples
    --------
    >>> d.keys()
    ['dim2', 'dim0', 'dim1', 'aux0', 'cm0']
    >>> d.get_keys('dim')
    ['dim2', 'dim0', 'dim1']
    >>> d.get_keys('aux|dim')
    ['dim2', 'dim0', 'dim1', 'aux0']
    >>> d.get_keys('dim[12]')
    ['dim2', 'dim1']

        '''
        if regex is None:
            return self.keys()

        keys = []        
        for key in self:
            if re.search('^%s' % regex, key):
                keys.append(key)
        
        return keys
    #--- End: def CfDict.get_keys

    def ncatts(self):
        return set(())
    
    def properties(self, nc=False):
        # __doc__ is copied from _find_properties.__doc__
        
        return _find_properties(self, nc=nc)
    #--- End: def CfDict.properties
    properties.__doc__ = _find_properties.__doc__

#--- End class CfDict


class Grid(CfDict):
    '''

    Grid(*args, **kwargs)

    A CF grid object defining a space's dimensionality, coordinates,
    cell measures and transformations in a dictionary-like object.

    Grid structure
    --------------
    A grid is composed as follows (ignoring attributes not composed
    from `cf` classes):
    
    grid --+--> dimensionality
           |
           +--> coordinates ---> coordinate bounds
           |
           +--> cell measures
           |
           +--> transforms
   
    The grid must have dimensionality attributes but all other
    components are optional. Refer to the `cf` module for more
    details.

    Dimensionality
    --------------
    The dimensions of the grid, and of its associated space, are given
    by the `dimension_sizes` and `dimension` attributes.

    The `dimension_sizes` attribute is a dictionary whose keys are
    dimension identifiers and values are positive integer dimension
    sizes. A dimension identifier is the string 'dim' suffixed by an
    arbitrary, but unique, integer. For example:

    >>>> g
    <CF Grid: (30, 24, 1, 17)>
    >>> g.dimension_sizes
    {'dim0': 1, 'dim1': 17, 'dim2': 30, 'dim3': 24}

    The `dimension` attribute specifies which dimensions relate to
    each grid component (coordinates and cell measures) and to the
    data array of the space which holds the grid. For example:

    >>> g.dimensions
    {'data': ['dim1', 'dim2', 'dim3'],
     'aux0': ['dim2', 'dim3'],
     'aux1': ['dim2', 'dim3'],
     'cm0' : ['dim2', 'dim3'],
     'dim0': ['dim0'],
     'dim1': ['dim1'],
     'dim2': ['dim2'],
     'dim3': ['dim3']}

    Each value of this dictionary is an ordered list which corresponds
    to the shape of each component.

    Note that if the grid contains dimensions of size 1 then the data
    of the space may have a lesser dimensionality then the grid (see
    the 'data' key in the above example).

    Note that it is possible for a grid dimension to have no
    associated grid components.

    Storage of coordinates and cell measures variables
    --------------------------------------------------
    Keys of the grid are identifiers for each of grid's coordinate and
    cell measures variables. The key values are the variable objects
    themselves. The key names are strings which describe which type of
    variable they store. Recognized types are:

    aux : Auxiliary coordinate
    cm  : Cell measures
    dim : Dimension coordinate

    A key string comprises one of these prefixes followed by a
    non-negative integer to discern between grid components of the
    same type. For example, a grid's keys may be:

    >>> sorted(g.keys())
    ['aux0', 'aux1', 'cm0', 'dim0', 'dim1', 'dim2', 'dim3']

    The non-negative integer suffixing each key is arbitrary but must
    be unique amongst keys of the same type.

    Note that, similarly to the storage of dimension coordinates in a
    netCDF file, a dimension coordinate's dimension name must be the
    same as its grid identifier.

    Transformations
    ---------------
    If a coordinate has an associated transformation (such as
    projection parameters or a derived dimensional coordinate), then
    it will have a `transform` attribute whose value is a key of the
    grid's `transform` attribute, which is a cf dictionary. The values
    of this cf dictionary are `cf.Transform` objects which contain the
    information required to realise the transformation. Refer to
    `cf.Transform` for more details. For example:

    >>> dump(g.transform['atmosphere_sigma_coordinate'])
    atmosphere_sigma_coordinate transform
    -------------------------------------
    Transform['ps'] = <CF Space: surface_air_pressure(30, 24)>
    Transform['ptop'] = <CF Space: ptop(1)>
    Transform['sigma'] = 'dim0'
    
    Virtual coordinates
    -------------------
    If a coordinate has a `transform` attribute but no data and no
    specified dimensions (i.e. its entry in the grid's `dimensions`
    dictionary attribute is an empty list) then it should be
    considered a container for the coordinate implied by the named
    transformation, should it be realized.

    Parameters
    ----------
    *args, **kwargs :
        Keys and values are initialized exactly as for a built-in
        dict.

    Overloaded operators
    --------------------
    The `in` (set membership) operator is overloaded to use
    numerically tolerant equality.

    Attributes
    ----------  
    dimension_sizes : dict
        See the 'Dimensionality' section.
    dimensions : dict
        See the 'Dimensionality' section.                                             
    nc_dimensions : dict, optional
        The netCDF dimension name for each dimension in the
        `dimension_sizes` attribute. If present when the grid is
        written to a netCDF file, then used for output netCDF
        dimension names.
    transform : dict, optional
        A cf dictionary of cf.Transform objects referred to by grid
        coordinates with a `transform` attribute. See the
        'Transformations' section for more details.
    _atol : None or float, optional
        Absolute tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.
    _rtol : None or float, optional
        Relative tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.

    Methods
    -------
    clear      : As for a built-in dict.
    copy       : Create a deep copy.
    coord      : Return a coordinate identified by its name.
    dump       : Return a string containing a full description of the
                 object.
    equals     : Determine whether two instances are congruent.
    get        : As for a built-in dict.
    get_keys   : Return grid keys which match a pattern.
    has_key    : As for a built-in dict but using numerically tolerant
                 equality.
    items      : As for a built-in dict.
    iteritems  : As for a built-in dict.
    iterkeys   : As for a built-in dict.
    itervalues : As for a built-in dict.
    keys       : As for a built-in dict.
    pop        : As for a built-in dict.
    popitem    : As for a built-in dict.
    properties : Return a set of writable, public attributes.
    setdefault : As for a built-in dict.
    update     : As for a built-in dict.
    values     : As for a built-in dict.

    '''
    def __init__(self, *args, **kwargs):

        # Create the dimension dictionaries
        self.dimension_sizes = {}
        self.dimensions      = {}

        super(Grid, self).__init__(*args, **kwargs)
    #--- End: def Grid.__init__

    def __repr__(self):

        shape = tuple(self.dimension_sizes.values())
        return '<CF %s: %s>' % (self.__class__.__name__, shape)
    #--- End: def Grid.__repr__

    def __str__(self):

        # Note that the dodgy print statement you may see probably
        # comes from netCDF4.netcdftime.DateFromJulianDay (version
        # 0.9.3). It's gone by 0.9.5.

        def _print_coord(grid, coord, dimension_coord):
            '''Private function called by Grid.__str__'''
            try: 
                self[coord]
            except KeyError: 
                shape = str(grid.dimension_sizes[coord])                
                x = coord+'('+shape+')'
                return x       

            if dimension_coord:       
                shape = str(int(grid[coord].size))
                x = grid[coord].name(ncvar=True, default=coord)+'('+shape+')'
            else:
                shape = grid.dimensions[coord][:]
                for i in xrange(len(shape)):
                    try:
                        shape[i] = grid[shape[i]].name(ncvar=True, default=shape[i])
                    except KeyError:
                        pass
                shape = str(tuple(shape)).replace("'", "")
                shape = shape.replace(',)', ')')
                try:
                    x = grid[coord].name(ncvar=True, default=coord) + shape
                except TypeError:
                    x = coord + shape

            try:
                self[coord].compress
            except AttributeError:
                try:
                    units = ' '+grid[coord].units
                except AttributeError:
                    units = ''   
                else:
                    if units == ' 1':
                        units = ''
                try:
                    axis = grid[coord].axis
                except AttributeError:
                    axis = ''   

                first = grid[coord].first_datum
                last  = grid[coord].last_datum
                if (grid[coord].name() == 'time' or axis == 'T') and units != '':
                    try:
                        calendar = grid[coord].calendar
                    except AttributeError:
                        calendar = 'standard'                    
                    first = str(netCDF4.num2date(first, units, calendar))
                    last  = str(netCDF4.num2date(last , units, calendar))
                    if grid[coord].size == 1:
                        x += " -> %s UTC" % first
                    else:
                        x += " -> %s to %s UTC" % (first, last)
                elif grid[coord].dtype.char == 'S':
                    # String valued coordinate's first and last values
                    if grid[coord].size == 1:
                        x += " -> '%s'%s" % (first, units)
                    else:
                        x += " -> '%s' to '%s'%s" % (first, last, units)
                else:
                    # Numeric valued coordinate's first and last values
                    if grid[coord].size == 1:
                        x += ' -> %.7g%s' % (first, units)
                    else:
                        x += ' -> %.7g to %.7g%s' % (first, last, units)
            else:
                x += ' -> compressed '
                compressed = []
                for unc in grid[coord].compress:
                    shape = str(unc.size)
                    compressed.append(unc.name(ncvar=True,
                                               default='unc')+'('+shape+')')
                x += ', '.join(compressed)

            return x
        #--- End def _print_coord

        string = 'Dimensions      : '
        x = []
        for coord in sorted(self.dimension_sizes):
            x.append(_print_coord(self, coord, True))
        string += '\n                : '.join(x) + '\n'

        string += 'Auxiliary coords: '
        x = []
        for coord in sorted(self.get_keys('aux')):
            x.append(_print_coord(self, coord, False))
        string += '\n                : '.join(x) + '\n'

        cell_measures = self.get_keys('cm')
        if cell_measures:
            string += 'Cell measures   : '
            x = []
            for cm in sorted(cell_measures):
                x.append(_print_coord(self, cm, False))
            string += '\n                : '.join(x) + '\n'
        
        return string
    #--- End: Grid.__str__

    def dump(self, id=None, nc=False, omit=()):
        '''
        
    Return a string containing a full description of the grid.
    
    Parameters
    ----------
    id : str, optional
        Set the common prefix of each line of the output. If None then
        defaults to the class name.
    nc : bool, optional
        If True then include attributes whose names begin 'nc'.        
    omit : sequence, optional
        Omit the given attributes from the description.

    Returns
    -------
        A string containing the description of the grid.

    See also
    --------
    cf.dump
    '''
        if id is None:
            id = self.__class__.__name__
            
        string ='grid properties\n---------------\n'

        properties = \
            self.properties(nc=nc)           - \
            set(('dimensions', 'transform')) - \
            set(omit)

        for prop in sorted(properties):
            string += '%s.%s = %s\n' % (id, prop, getattr(self, prop).__repr__())
        string += '\n'

        for key in sorted(self.dimensions):
             string += "%s.dimensions['%s'] = %s\n" % (id, key, self.dimensions[key])
        string += '\n'
        
        for key in sorted(self.get_keys('dim')) + \
                sorted(self.get_keys('aux')) + \
                sorted(self.get_keys('cm')):
            string += self[key].dump(id=id+"['"+key+"']", nc=nc)+'\n'
            
        if hasattr(self, 'transform'):
            for key in self.transform:
                string += self.transform[key].dump(id=id+".transform['"+key+"']", 
                                                   nc=nc)+'\n'

        return string
    #--- End: def Grid.dump

    def equals(self, other, rtol=None, atol=None, override=True): 
        '''

    Return True if two instances are congruent in that, for each group
    of grid component types (dimension coordinate, auxiliary
    coordinate, cell measures, etc.) there are an equal number of keys
    and each key's value equals a value in the other grid. Note that
    within a group of component types, the key names need not be the
    same.
      
    Equality of numbers is to within a tolerance. Refer to `cf` for
    details.

    Arguments
    ---------
    other : Grid
        The grid to compare against.
                                                            
    Parameters                                                             
    ----------                                                             
    atol : None or float, optional                                         
        If None then use the default method for setting the absolute       
        tolerance for equality of real numbers (refer to `cf` for          
        details). If a float then set the absolute tolerance to this       
        value for all comparisons (refer to the `override` parameter).     
    override : bool, optional                                              
        If False then only use a float value of the `rtol` or `atol`       
        parameters if it can not be set from attributes of the objects     
        being compared.                                                    
    rtol : None or float, optional                                         
        If None then use the default method for setting the relative       
        tolerance for equality of real numbers (refer to `cf` for          
        details). If a float then set the relative tolerance to this       
        value for all comparisons (refer to the `override` parameter).     
     
    Returns
    -------
        True if the two instances are congruent, False otherwise.

        '''
        if id(self) == id(other):
            return True
        
        # Check that each instance is the same type
        if type(self) != type(other):
            return False

        try:
            if set(self.dimension_sizes) != set(other.dimension_sizes):
                return False
        except AttributeError:
            pass

        special_properties = set(('dimensions', 'dimensions_sizes'))

        self_properties  = self.properties()  - special_properties
        other_properties = other.properties() - special_properties

        if self_properties != other.properties():
            return False
        
        for prop in self_properties:
            x = getattr(self, prop)
            y = getattr(other, prop)
            if not equals(x, y, rtol=rtol, atol=atol, override=override):
                return False

        for coord_type in ('dim', 'aux', 'cm'):
            self_keys  = sorted(self.get_keys(coord_type))
            other_keys = sorted(other.get_keys(coord_type))
            for coord0 in self_keys:
                found_match = False
                for coord1 in other_keys: 
                    if equals(self[coord0], other[coord1], 
                              rtol=rtol, atol=atol, override=override):
                        found_match = True
                    if found_match:
                        other_keys.remove(coord1)                
                        break
                if not found_match:
                    return False

        return True
    #--- End: def Grid.equals

    def coord(self, name, role=None, key=False, exact=False):
        '''

    Find a coordinate of the grid by name.

    The given `name` argument is an abbreviation for (or equal to if
    the `exact` parameter is True) its `standard_name` attribute. If
    the `key` parameter is True then return the coordinate's grid key
    name. If a coordinate does not have `standard_name` attribute,
    then its `ncvar` attribute is used.

    Note that the returned coordinate is an object identity to the
    coordinate stored in the grid so, for example, a coordinate's
    attributes may be changed in-place as follows:

    >>> g.coord('height').long_name
    AttributeError: 'Coordinate' object has no attribute 'long_name'
    >>> g.coord('hei').long_name = 'HEIGHT'
    >>> g.coord('heigh').long_name
    'HEIGHT'

    Or a deep copy may be made with the coordinate's `copy` method:

    >>> h = g.coord('height').copy()

    Arguments
    ---------
    name : str
        The string to identify a coordinate by name.

    Parameters
    ----------
    exact : bool, optional
        If True then assume that the value of the `name` argument is
        equal to exactly one coordinate's name.
    key : str, optional
        Return the grid key name instead of the coordinate.
    role : str or None, optional
        Restrict the search to coordinates of the given role. Valid
        values are 'dim' and 'aux', for dimension and auxiliary
        coordinate types respectively. If None then both types of
        coordinates will be searched.
       
    Returns
    -------
        If a coordinate has been identified, return either a
        `cf.Coordinate` instance or, if the `keys` parameter is True,
        a grid key name string. otherwise, return None.

    Examples
    --------
    >>> g.coord('lon')
    <CF Coordinate: longitude(128)>
    >>> g.coord('lon', key=True)
    'dim2'
    >>> g.coord('lonX', key=True)
    None
    >>> g.coord('lon', exact=True)
    None
    >>> g.coord('longitude', exact=True)
    <CF Coordinate: longitude(128)>

    '''
#        name_is_int = isinstance(name, int)
#        if name_is_int and role is None:
#            raise RuntimeError('Must set role when giving integer argument')
        
        dim, aux = True, True
        if role == 'dim':
            aux = False
        elif role == 'aux':
            dim = False
                    
        if name is None:
            return None

        # exact, e.g. name='latitude'
        if dim:
            dim_list = self.get_keys('dim')
#            if name_is_int:
#                # e.g. name = 2
#                dim = 'dim'+repr(name)
#                if dim in dim_list:
#                    if key:
#                        return dim
#                    else:
#                        return self[dim]
                    
            for dim in dim_list:
                if name == self[dim].name(ncvar=True, default=None):
                    # e.g. name = 'latitude'
                    if key:
                        return dim
                    else:
                        print 'sadsdfsd'
                        return self[dim]
               
        if aux:
            aux_list = self.get_keys('aux')
#            if name_is_int:
#                # e.g. name=2
#                aux = 'aux'+repr(name)
#                if aux in aux_list:
#                    if key: 
#                        return aux
#                    else:
#                        return self[aux]

            for aux in aux_list:
                if name == self[aux].name(ncvar=True, default=None):
                    # e.g. name='longitude'
                    if key: 
                        return aux
                    else:
                        return self[aux]

        # Still here?
        if exact:
            return None

        # Still here? Look for partial matches            
        found_name = 0
        get = []
        if dim:
            get.extend(dim_list)
        if aux:
            get.extend(aux_list)
        for key1 in get:
            full = self[key1].name(ncvar=True, default=None)
            if full is None: 
                continue
            if re.search('^%s' % name, full):
#            if full.startswith(name):
                # e.g. name='lati'
                coord = key1
                found_name += 1                

        if found_name != 1:
            return None

        if key:
            return coord
        else:
            return self[coord]
    #--- End: def Grid.coord            

#--- End: class Grid

class Transform(CfDict):
    '''

    Transform(*args, **kwargs)

    A CF transform object defining a coordinate's transformation in a
    dictionary-like object.

    The named parameters and their values of the transformation
    (i.e. the transformation's mappings) comprise the object's
    key-value pairs.

    A transformation is equivalent to either a netCDF(CF)
    'formula_terms' or 'grid_mapping' property. The latter is
    identified by the presence of the 'grid_mapping_name' key which
    contains the mapping's name.

    In the 'formula_terms' case, a mapping to a coordinate uses the
    coordinate's grid key name as a value. A mapping to another space
    might be a shallow copy of another space in memory.

    Parameters
    ----------
    *args, **kwargs :
        Keys and values are initialized exactly as for a built-in
        dict.
 
    Attributes
    ----------
    transform_name : str, optional         
        The identifying name of the transformation. Typically a
        netCDF(CF) 'formula_terms' or 'grid_mapping_name'.
    _atol : None or float, optional
        Absolute tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.
    _rtol : None or float, optional
        Relative tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.

    Methods
    -------
    clear      : As for a built-in dict.
    copy       : Create a deep copy.
    dump       : Return a string containing a full description of the
                 object.
    equals     : Determine whether two instances are congruent with
                 each other.
    get        : As for a built-in dict.
    get_keys   : Return grid keys which match a pattern.
    has_key    : As for a built-in dict but using numerically tolerant
                 equality.
    items      : As for a built-in dict.
    iteritems  : As for a built-in dict.
    iterkeys   : As for a built-in dict.
    itervalues : As for a built-in dict.
    keys       : As for a built-in dict.
    pop        : As for a built-in dict.
    popitem    : As for a built-in dict.
    properties : Return a set of writable, public attributes.
    setdefault : As for a built-in dict.
    update     : As for a built-in dict.
    values     : As for a built-in dict.

    Overloaded operators
    --------------------
    The `in` (set membership) operator is overloaded to use
    numerically tolerant equality.

    Examples
    --------
    >>> t
    <CF Transform: atmosphere_sigma_coordinate>
    >>> dump(t)
    atmosphere_sigma_coordinate transform
    -------------------------------------
    Transform['ps'] = <CF Space: surface_air_pressure(73, 96)>
    Transform['ptop'] = 0.05
    Transform['sigma'] = 'dim0'

    >>> t
    <CF Transform: rotated_latitude_longitude>
    >>> dump(t)
    rotated_latitude_longitude transformation
    -----------------------------------------
    Transform['grid_mapping_name'] = 'rotated_latitude_longitude'
    Transform['grid_north_pole_latitude'] = 33.67
    Transform['grid_north_pole_longitude'] = 190.0

    '''
    def __repr__(self):
        
        try:
            return '<CF %s: %s>' % (self.__class__.__name__, 
                                    self.transform_name)
        except AttributeError:
            return '<CF %s: >' % self.__class__.__name__
    #--- End: def Transform.__repr__

    def __str__(self):
        
        try:
            return 'Transform       : '+self.transform_name
        except AttributeError:
            return 'Transform       : '+repr(self)
    #--- End: def Transform.__str__

    def dump(self, id=None, nc=False, omit=()):
        '''

    Return a string containing a full description of the
    transformation.

    Parameters
    ----------
    id : str, optional
        Set the common prefix of each line of the output. If None then
        defaults to the class name.
    nc : bool, optional
        If True then include attributes whose names begin 'nc'.        
    omit : sequence, optional
        Omit the given attributes from the description.

    Returns
    -------
        A string containing the description of the object.

    See also
    --------
    cf.dump
    '''
        if id is None:
            id = self.__class__.__name__
            
        try:
            string = self.transform_name + ' transformation\n'
        except AttributeError:
            string = ' transformation\n'
        string += ''.ljust(len(string)-1, '-') + '\n'

        for prop in sorted(self.properties(nc=nc) - set(omit) - \
                               set(('transform_name',))):
            string += '%s.%s = %s\n' % (id, prop, repr(getattr(self, prop)))
            
        for key in sorted(self.keys()):
            string += "%s = %s\n" % (id+"['"+key+"']", repr(self[key]))

        return string
    #--- End: def Transform.dump

#--- End: class Transform


class CellMethods(CfList):
    '''

    CellMethods(sequence=())

    A CF cell methods object to describe the characteristic of a field
    that is represented by cell values.

    Each cell method is stored in a dictionary and these dictionaries
    are stored in a list-like object. Similarly to a CF 'cell_methods'
    string, the order of cell methods in the list is important.

    The dictionary representing each cell method recognizes the
    following keys (where all keys are optional; the referenced
    sections are from NetCDF Climate and Forecast (CF) Metadata
    Conventions Version 1.5; and words in double quotes ("...") refer
    to CF cell_methods components in the referenced sections):

    Key       Value 
    --------- --------------------------------------------------------
    method    A "method" component.
              Refer to section 7.3.
    
    name      A list of all of the "name" components involved with the
              "method" component. Each element is either:

              1. The standard_name of the cell method's dimension
              2. The string 'area'          
              3. A netCDF variable name or netCDF dimension name

              Option 3. only occurs if the cell method's dimension
              either has no dimension coordinate or has a dimension
              coordinate with no standard_name.

              This is a deviation from the "name" components as
              described in sections 7.3, 7.3.1 and 7.3.4, in that
              standard_names are stored whenever possible, rather than
              only in the special cases described in section
              7.3.4. Inspection of the 'no_coords' list retains the
              information required to fully interpret the cell method.
    
    dim       A list of space dimension names corresponding to the
              'name' list. A 'name' list value of 'area' always
              corresponds to a 'dim' list value of None.
    
    no_coords A list of booleans, corresponding to the 'name' list,
              indicating whether or not a particular cell method is
              relevant to the data in a way which may not be precisely
              defined by the corresponding dimension or dimensions.
              Refer to section 7.3.4.
    
    interval  A list of "interval" component values corresponding to
              the 'name' list.
              Refer to section 7.3.2.
    
    units     A list of "interval" component units corresponding to
              the 'interval' list.
              Refer to section 7.3.2.
    
    comment   A "comment" or non-standardised information, component.
              Refer to sections 7.3.2 and 7.4
    
    within    A "within" climatology component.
              Refer to section 7.4.
    
    over      An "over" cell portion or "over" climatology component.
              Refer to sections 7.3.3 and 7.4.
    
    where     A "where" component.
              Refer to section 7.3.3.
    --------- --------------------------------------------------------

    Note that the above table assumes that the cell methods have been
    constructed in the context of a space, the only way in which the
    'dim' and 'no_coords' keys may take sensible values. If this is
    not the case, as would happen if the `parse` method were called
    without its `space` keyword, then elements of the 'dim' key
    default to False and elements of the 'no_coords' key default to
    None.

    Parameters
    ----------
    sequence : iterable, optional
        Initialize new list from sequence's items.

    Examples
    --------
    >>> c = CellMethods()
    >>> c = c.parse('time: minimum within years time: mean over years (ENSO years)')    
    >>> print c
    Cell methods    : time: minimum within years
                      time: mean over years (ENSO years)
    >>> list c
    [
     {'method'   : 'minimum',
      'name'     : ['time'],     
      'dim'      : [False],
      'no_coords': [None], 
      'within'   : 'years'
     },
     {'method'   : 'mean'
      'name'     : ['time'],
      'dim'      : [False], 
      'no_coords': [None],     
      'comment'  : 'ENSO years', 
      'over'     : 'years'
     }
    ]

    >>> c = c.parse('lat: lon: standard_deviation')
    [
     {'dim'      : [False, False],
      'method'   : 'standard_deviation'
      'name'     : ['lat', 'lon'],
      'no_coords': [None, None],
     }
    ]
     
    Overloaded operators
    --------------------
    Refer to `cf.CfList`.
 
    Attributes
    ----------
    _atol : None or float, optional
        Absolute tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.
    _elements_atts : bool
        If True then do not raise an exception when requesting a
        non-existent attribute, but broadcast the request to each
        element of the list.
    _rtol : None or float, optional
        Relative tolerance for numeric equality. Unset is equivalent
        to None, which implies a default value. Refer to `cf`.

    Methods
    -------
    append     : As for a built-in list.
    copy       : Create a deep copy.
    count      : As for a built-in list but using numerically tolerant
                 equality.
    dump       : Return a string containing a full description of the
                 object.
    equals     : Determine whether two lists are congruent element-wise.
    extend     : As for a built-in list.
    index      : As for a built-in list but using numerically tolerant
                 equality.
    insert     : As for a built-in list.
    parse      : Parse a netCDF(CF) cell_methods string.
    pop        : As for a built-in list.
    properties : Return a set of writable, public attributes.
    remove     : As for a built-in list.
    reverse    : As for a built-in list.
    sort       : As for a built-in list.
    strings    : Convert each element to a string.

    '''
    def __repr__(self):

        try:
            dims = [', '.join(x['name']) for x in self]
        except TypeError:
            dims = [', '.join(x['dim']) for x in self]            

        return '<CF %s: %s>' % (self.__class__.__name__, ', '.join(dims))
    #--- End: def CellMethods.__repr__

    def __str__(self):
        
        strings = self.strings()

        try:
            strings[0]
        except IndexError:
            pass
        else:
            for i in xrange(1,len(strings)):            
                strings[i] = '                  ' + strings[i]
            
        return 'Cell methods    : ' + '\n'.join(strings) + '\n'
    #--- End: def CellMethods.__str__

    def dump(self, id=None, nc=False, omit=()):
         '''

    Return a string containing a full description of the cell methods.

    If a cell methods 'name' is followed by a '*' then that cell
    method is relevant to the data in a way which may not be precisely
    defined its corresponding dimension or dimensions.

    Parameters
    ----------
    id : str, optional
        Set the common prefix of each line of the output. If None then
        defaults to the class name.
    nc : bool, optional
        Dummy argument required for consistency with the `cf.dump`
        function.
    omit : sequence, optional
        Dummy argument required for consistency with the `cf.dump`
        function.

    Returns
    -------
        A string containing the description of the cell methods.

    See also
    --------
    cf.dump
    '''
         if id is None:
             id = self.__class__.__name__
             
         strings = self.strings()
         
         for i in xrange(len(strings)):            
             strings[i] = id+'['+repr(i)+'] -> ' + strings[i]
             
         return 'cell methods\n------------\n'+'\n'.join(strings)+'\n'
    #--- End: def CellMethods.dump

    def parse(self, string, space=None):
        '''

    Parse a CF cell_methods string into this CellMethods instance.

    Arguments
    ---------
    string : str
        The CF cell_methods string to be parsed into the CellMethods
        object.

    Keywords
    --------
    space : Space, optional
        Set the 'name', 'dim' and 'no_coord' key values appropriately
        for the given space. Refer to `cf.CellMethods`.

    Returns
    -------
        A cell methods list.

    Examples
    --------
    >>> c = CellMethods()
    >>> c = c.parse('time: minimum within years time: mean over years (ENSO years)')    
    >>> print c
    Cell methods    : time: minimum within years
                      time: mean over years (ENSO years)
    
    '''
        result = type(self)()

        # Split the cell_methods string into a list of strings ready
        # for parsing into the result list. E.g. 'lat: mean (interval:
        # 1 hour)' -> ['lat:', 'mean', '(', 'interval:', '1', 'hour',
        # ')']
        cell_methods = re.sub('\((?=\w)' , '( ', string)
        cell_methods = re.sub('(?<=\w)\)', ' )', cell_methods).split()
        
        while cell_methods:

            # Create a new element for this 
            result.append(CfDict())            

            # List of names
            result[-1]['name']      = CfList()
            result[-1]['dim']       = CfList()
            result[-1]['no_coords'] = CfList()

            while cell_methods:
#                if not re.search(':$', cell_methods[0]):
                if not cell_methods[0].endswith(':'):
                    break
                result[-1]['name'].append(cell_methods.pop(0)[:-1])            
                result[-1]['dim'].append(False)
                result[-1]['no_coords'].append(None)
                            
            if not cell_methods:
                break

            # Method
            result[-1]['method'] = cell_methods.pop(0)

            if not cell_methods:
                break

            # Climatological statistics and statistics which apply to
            # portions of cells
            while cell_methods[0] in ('within', 'where', 'over'):
                term = cell_methods.pop(0)
#                if term == 'over' and cell_methods[0] in ('years', 'days'):
#                    result[-1]['c'+term] = cell_methods.pop(0)
#                else:
                result[-1][term] = cell_methods.pop(0)
                if not cell_methods:
                    break

            if not cell_methods: 
                break

            # intervals and comment
#            if re.search('\($', cell_methods[0]):
            if cell_methods[0].endswith('('):
                cell_methods.pop(0)

                if not (re.search('^(interval|comment):$', cell_methods[0])):

                    cell_methods.insert(0, 'comment:')
                           
                while not re.search('^\)$', cell_methods[0]):
                    
                    term = cell_methods.pop(0)[:-1]
                    
                    if term == 'interval':
                        if not result[-1].has_key(term):
                            result[-1][term]    = CfList()
                            result[-1]['units'] = CfList()
                        result[-1][term].append(float(cell_methods.pop(0)))
                        if re.search('^\w*$', cell_methods[0]):
                            result[-1]['units'].append(cell_methods.pop(0))
                        else:
                            result[-1]['units'].append('')
                        #--- End: if ... else ...
                        continue

                    if term == 'comment':
                        comment = CfList()
                        while cell_methods:
#                            if re.search('\)$', cell_methods[0]):
                            if cell_methods[0].endswith(')'):
                                break
#                             if re.search(':$' , cell_methods[0]):
                            if cell_methods[0].endswith(':'):
                                break
                            comment.append(cell_methods.pop(0))
                        result[-1]['comment'] = ' '.join(comment)
                            
#                if re.search('\)$', cell_methods[0]):
                if cell_methods[0].endswith(')'):
                    cell_methods.pop(0)
            #--- End: if cell_methods[0].endswith('('):

        #--- End: while cell_methods:

        # ----------------------------------------------------------
        # If no space has been provided, then return now without
        # adjusting the 'name', 'dim' and 'no_coords' keys
        # ----------------------------------------------------------
        if space is None:
            return result

        # ----------------------------------------------------------
        # Still here? Then adjust the 'name', 'dim' and 'no_coords'
        # keys according to the given space.
        # ----------------------------------------------------------
        try: 
            space.grid.nc_dimensions
        except AttributeError:
            return result

        # Change each 'name' value to a standard_name (or grid
        # coordinate key) and create the 'dim' key
            
        # From the CF conventions (1.5): In the specification of this
        # attribute, name can be a dimension of the variable, a scalar
        # coordinate variable, a valid standard name, or the word
        # 'area'.
        for j in xrange(len(result)):
            for i in xrange(len(result[j]['name'])):

                # Since input scalar coordinate variables have now
                # been converted into 1d dimension coordinates, name
                # being "a dimension of the variable, a scalar
                # coordinate variable" may be checked by testing name
                # against space.grid.nc_dimensions.values().                   
                found_ncdimension = False
                for dim, ncdim in space.grid.nc_dimensions.iteritems():
                    if result[j]['name'][i] == ncdim:
                        result[j]['no_coords'][i] = False
                        result[j]['dim'][i]       = dim
                        try:                            
                            result[j]['name'][i] = space.grid[dim].name(default=ncdim)
                        except KeyError:
                            result[j]['name'][i] = ncdim
                        found_ncdimension = True
                        break

                if found_ncdimension:
                    continue

                # The cell_methods "name" component is either a
                # standard_name or the string 'area'
                result[j]['no_coords'][i] = True
                if result[j]['name'][i] == 'area':
                    result[j]['dim'][i] = None
                else:
                    for sn in VariableList(space.grid.values()).standard_name:
                        if result[j]['name'][i] == sn:
                            dim = space.grid.coord(sn, key=True, exact=True)
                            result[j]['dim'][i] = dim
                            break
            #--- End: for i
        #--- End: for j

        return result
    #--- End: def CellMethods.parse

    def strings(self):
        '''

    Convert each element of the cell methods list to a netCDF(CF)
    cell_methods-like string.

    Note that if the intention is to concatenate the output list into
    a string for creating a netCDF(CF) cell_methods attribute, then
    the cell methods "name" components may need to be modified, where
    appropriate, to reflect netCDF variable names. This is done
    automatically by the `cf.write` function when creating a file on
    disk.

    Returns
    -------
        A built-in list of cell method strings.

    Examples
    --------
    >>> c = CellMethods()
    >>> c = c.parse('time: minimum within years time: mean over years (ENSO years)')    
    >>> print c
    Cell methods    : time: minimum within years
                      time: mean over years (ENSO years)
    >>> c.strings()
    ['time: minimum within years',
     'time: mean over years (ENSO years)']

     '''
        strings = []
        
        i = 0
        for cm in self:

            strings.append('')

            for j in xrange(len(cm['name'])):
                strings[i] += cm['name'][j]
#                if cm['no_coords'][j]:
#                    strings[i] += '*'
                strings[i] += ': '

            strings[i] += cm['method']    

            x = []
            try:
                x.extend(('within', cm['within'])) 
            except KeyError:
                try:
                    x.extend(('where', cm['where'])) 
                except KeyError:
                    pass
            try:
                x.extend(('over', cm['over'])) 
            except KeyError:
                pass
            if x:
                strings[i] += ' ' + ' '.join(x)

            if cm.has_key('interval'):
                strings[i] += ' ('
                for j in xrange(len(cm['interval'])):
                    strings[i] += 'interval: '+repr(cm['interval'][j])
                    try:
                        strings[i] += ' '+cm['units'][j]
                    except KeyError:
                        pass
                if cm.has_key('comment'):
                    strings[i] += ' comment: '+cm['comment']  
                strings[i] += ')'

            elif cm.has_key('comment'):
                strings[i] += ' ('+cm['comment'] +')'
            
            i += 1   
        #--- End: for cm

        return strings
    #--- End: def CellMethods.strings

#--- End: class CellMethods
